Live data from Hacker News

Functional Programming Self-Affirmations

norikitech.com

111–119 of 119 posts

Re: Functional Programming Self-Affirmations

#111

Earlier quoted context omitted.

> How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system? I think you're confusing "make illegal states unrepresentable" with "parse, don't verify"? If your type cannot represent any invalid states, there's no way you can reach them through mutation.

The "sequences of mutations" phrasing made me think they were talking about stuff like state machines or handles for external resources—for example calling `databaseConnection.close()` on an already-closed connection, which is usually a runtime error (or maybe a no-op).

I don't see the problem with state machines. When you're dealing with something that handles external input, "the input is invalid" is, for your program, a valid state. For example a regular expression engine doesn't try to make it impossible to pass in a string that doesn't match; it just returns a result indicating that the string didn't match.

Database connections are exactly what the "functional core, imperative shell" principle is about. The idea is to handle all I/O at the boundary. So shell is never passing open database connections into the functional core; it's instead retrieving everything that's needed up front so that the core can be deterministic.

"Functional core, imperative shell" might actually be my favorite of the principles, because it makes code so much easier to test. Every time I come across a codebase whose test suite has to make intense use of mocking to cope with how they allowed concurrency to spread throughout every single layer and module in the application, I get a little bit sad that nobody did its authors the service of teaching them that you don't actually need to make your own life hard like that.

It also tends to result in less code to understand and maintain overall, IME. Because if you limit the number of places where an error is even possible, you don't get stuck having to litter your codebase with excess (and often repetitive) error handling code.

Re: Functional Programming Self-Affirmations

#112

Earlier quoted context omitted.

And the Make Impossible States Unrepresentable crowd program like that?

It is not too bad in languages with discriminated unions. It's also not hard to fake discriminated unions in languages without them, even if you will miss some of the niceties. Rather than thinking of it as an enum, think of it as a list of contructors: class ProgramState { bool w, x, y, z; ProgramState(x, z) // implies y = true, w = true ProgramState(w, z) // cannot set x; implies y = false (cannot set y) } Even if…

Also to mention it, languages without discriminated unions often have generics and function types, which can be used to build discriminated unions with Church encodings:

    // idiomatic typescript
    type Optional = 
     | {type: 'absent', detail: IfAbsent}
     | {type: 'present', value: IfPresent}
    
    // Church-encoded version
    type Either = (ifLeft: (x: x) => z, ifRight: (y: y) => z) => z
    
    // isomorphism between the two
    function church(opt: Optional): Either {
      return (ifLeft, ifRight) => opt.type === 'absent'? ifLeft(opt.detail) : ifRight(opt.value)
    }
    function unchurch(opt: Either): Optional {
      return opt>(x => ({type: 'absent', detail: x}), y => ({type: 'present', value: y}))
    }
In addition the Church encoding of a sum type, is a function that takes N handler functions and calls the appropriate one for the case that the data type is in. With a little squinting, this is the Visitor pattern.

    interface LeftRightVisitor {
      visit(x: Left): Z
      visit(y: Right): Z
    }
    interface LeftRight {
      accept(visitor: LeftRightVisitor): Z;
    }
    class Left implements LeftRight {
      constructor(public readonly x: X) {}
      accept(visitor: LeftRightVisitor) {
        return visitor.visit(this)
      }
    }
    class Right implements LeftRight {
      constructor(public readonly y: Y) {}
      accept(visitor: LeftRightVisitor) {
        return visitor.visit(this)
      }
    }
    // isomorphism
    function visitify(opt: Optional): LeftRight {
      return opt.type === 'absent' ? new Left(opt.detail) : new Right(opt.value)
    }
    function unvisitify(opt: LeftRight): Optional {
      return opt.accept({
        visit(value: Left | Right) {
          return value instanceof Left? {type: 'absent', detail: value.x} : {type: 'present', value: value.y}
        }
      })
    }
The main difference with the usual visitor pattern is that the usual visitor pattern doesn't return anything (it expects you to be holding some mutable state and the visitor will mutate it), you can do that too if you don't have access to a suitable generic for the Z parameter.

Re: Functional Programming Self-Affirmations

#113

Earlier quoted context omitted.

The "sequences of mutations" phrasing made me think they were talking about stuff like state machines or handles for external resources—for example calling `databaseConnection.close()` on an already-closed connection, which is usually a runtime error (or maybe a no-op).

I don't see the problem with state machines. When you're dealing with something that handles external input, "the input is invalid" is, for your program, a valid state. For example a regular expression engine doesn't try to make it impossible to pass in a string that doesn't match; it just returns a result indicating that the string didn't match. Database connections are exactly what the "functional core, imperative…

I agree with all of that, but I don't see what it has to do with this thread of the discussion.

The comment I replied to was wondering what exactly greener_grass was referring to when they said:

> outside of Functional Programming […] How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?

And my guess was that they had illegal transitions between states in mind. Those are hard/impossible to statically reason about when the program is written as "sequences of mutations" (particularly when aliasing is possible).

Re: Functional Programming Self-Affirmations

#114

Earlier quoted context omitted.

Non-locality of exceptions is a feature , not a bug. It's so you can focus on the success case, instead of error case, when reading your code. It's usually, but not always, what you want. "errors as values" is effectively the same thing as exceptions anyway, except it's explicit - meaning it's hurting readability by default and adding extra work[0]; modern languages go to extreme length to try and paper it over with…

In general, I find that explicit code is more easily read than implicit code. I prefer static over dynamic typing, I actually _like_ the explicitness of async/await or the IO monad. If something allows me to find out information about my current context without having to move up or down the stack and reading the code in other functions, I'm pretty happy about that something, because reading code is slow and tedious.…

> What is it about implicit code that makes you feel it's more readable?

It's more readable when I don't care about the implicit parts at the moment. It's less readable when I do. The key thing is, whether or not I care changes from task to task, or even within the task, possibly many times per day.

The problem with our current paradigm is that we want to work on a shared plaintext artifact (codebase), and we want it to simultaneously:

1) Express everything there is about the program;

2) Be easy to read and understand and modify by humans;

3) Be the same for everyone at all times - a shared single source of truth.

"Success path" logic, error handling, logging, async/await, authentication, etc. are all cross-cutting concerns. Now, 1) means we're forced to include all of them in code simultaneously, but this goes against 2). Like, when I'm trying to understand the overall logic of some business process, then error handling and async/await are irrelevant. They're pure noise. Yet 1) forces me to look and think about them at all times.

So the issue is, 2) is best achieved when you can "filter out" concerns you don't care about at a given moment, and operate on such simplified view of code. But 1) and 3) requires us to spell the all out, everywhere, at all times. The way this is mitigated today, is through ever more complex syntax and advanced mathematical trickery. Like your async/await keywords, or the IO monad. They're ways of compressing some concerns (or classes of concerns) into terse notation, but that comes at the cost of increased complexity and mental demand, too (I mean, explain to me what a monad is, again? :)). I believe that at this point, all modern languages are hitting the Pareto frontier of readability, so whether you use $whatever-routines instead of async, or result types instead of exceptions, it's all just making some cases more readable, at the expense of other cases.

In the same category of problems is also another "holy war": lots of small functions, vs. fewer big ones. There is no right answer here, because it depends on what your goal as a reader is at the moment - e.g. understanding the idea expressed by some logic may benefit from small functions, but debugging it often benefits from the opposite. This is, again, a faux problem, created by our tooling and insistence on the "shared plaintext single source of truth" paradigm.

Compare with code folding in IDEs. It's a view feature that lets you hide blocks of code - like loop bodies, classes, or function definitions - that you don't care about at the moment. Now imagine a similar feature existed, that would let you "fold away" error handling entirely. Or "fold away" the try/catch blocks, or all the mess of dealing with Result types. Or fold away logging. Or async/await. This is the solution we need - the ability to view the shared code through various lenses. Sacrificing 3) lets us get both 1) and 2) at the same time.

It's way better than what we do now, which is precommitting to relative importance of various cross-cutting concerns, by encoding them in how easy or hard they're to write in a given language.

Re: Functional Programming Self-Affirmations

#115

Earlier quoted context omitted.

Nobody says you have to have a single enum type containing all the combinations. No, no one would continue up to 2^16 and the code would get unmanageable long before that. But it's illustration of the problems starting out dealing with the invalid states of two variables using an enum because what happens when more and more variables arrive? Sure, the standard answer is "just refactor" but my experience is no client…

> and a trickle of binary conditions is a very common occurrence as is code expanding to handle these (and become excessively complicated). But you still have to handle this in your code. Wherever you have your conditions that handle this, your nest of if statements still need to cover all of these invalid combinations and ensure your app doesn't silently do the wrong thing or just crash (better). Changing requiremen…

Changing requirements requires changing code. I don't think it's a valid argument to say "we shouldn't take that approach because as requirements change we'll have to change the code". That's essentially software development.

You're misunderstanding me. Of course changing requirements mean changing code. The distinction is between a situation of "a small change in a requirement means a small change in code" and "a small change in a requirement means a BIG change in code". The make "make an enum of all the legal cases" approaches produces a situation where adding more binary conditions and requirements around them resulting in each change resulting a larger increase in code. And this in turn can result in a "we have to refactor this if we get one more change" and that's even more frustrating to those dictating requirements (and not uncommon in enterprise software).

Practically if you don't want to use enums and want another option, use a "builder" object. Pass in all of your booleans there and have it do you validation when you call its build method.

Cases are fine in some circumstances, enums of legal cases are fine in other circumstances and these builder might even be useful in a few bizarre cases. The main argument I'd have is that when one passes from "this approach can be useful, let's look at the situation" to "anything without this is bad", you often wind-up with a complete misallocation of resources and a fragile inability to make changes, as can be seen in today's legacy code (which was often yesterday's "best coding practices code").

Re: Functional Programming Self-Affirmations

#116
post #106

Earlier quoted context omitted.

> why not? what's the difference between those two categories, mentioned in your last two sentences, as far as this argument about illegal states is concerned? not clear to me. I kinda began my comment with that reason: The difficulty entirely depends on whether discarding the occasional invalid write is possible. If you can simply return an error and ignore the write/transaction, you're golden. If you can't, it beco…

Shouldn't an unrepresentable bad state not even have been proposed as a write tho? I mean the way I understand it, if something is trying to write a bad state somewhere, it is being represented somehow isn't it?

No, because you almost never have full data autonomy in corporate contexts. And the microservice arichtecures don't make this more robust either.

I.e. a transaction will have a matching transaction in another corporations/banks system, even if you don't have a distributed monolith unlike everyone else.

Re: Functional Programming Self-Affirmations

#117
post #82

Earlier quoted context omitted.

The difficulty of Making illegal state unrepresentable depends entirely on the domain you're working on. And whether discarding the occasional invalid transaction is viable. If you're writing a CMS/wiki software, it's gonna be pretty straightforward to do. If you're working with transactions, trades, contracts etc, it's not.

> If you're working with transactions, trades, contracts etc, it's not. I don’t mean to rain on your parade here, but there’s quite a few high powered orgs in the finance world that are well known for making extensive use of functional languages. Jane St is the most famous example but it’s not the only one. Standard Chartered Bank uses a lot of Haskell, as does Barclays and Bank of America.

Nobody in this thread talked about functional languages until you showed up.

Re: Functional Programming Self-Affirmations

#118
post #116

Earlier quoted context omitted.

Shouldn't an unrepresentable bad state not even have been proposed as a write tho? I mean the way I understand it, if something is trying to write a bad state somewhere, it is being represented somehow isn't it?

No, because you almost never have full data autonomy in corporate contexts. And the microservice arichtecures don't make this more robust either. I.e. a transaction will have a matching transaction in another corporations/banks system, even if you don't have a distributed monolith unlike everyone else.

I see, well, that is a good point. What is the point of any architectural consideration when you are forced to ingest garbage because others have not made any effort to properly architect their systems. Maybe a strong reason why worse is better in practice.

Re: Functional Programming Self-Affirmations

#119

Earlier quoted context omitted.

> and a trickle of binary conditions is a very common occurrence as is code expanding to handle these (and become excessively complicated). But you still have to handle this in your code. Wherever you have your conditions that handle this, your nest of if statements still need to cover all of these invalid combinations and ensure your app doesn't silently do the wrong thing or just crash (better). Changing requiremen…

Changing requirements requires changing code. I don't think it's a valid argument to say "we shouldn't take that approach because as requirements change we'll have to change the code". That's essentially software development. You're misunderstanding me. Of course changing requirements mean changing code. The distinction is between a situation of "a small change in a requirement means a small change in code" and "a sm…

I still feel like there is an important point you're walking past.

If there are 2^16 different combinations that are relevant, then you still need to handle these 2^16 combinations in your code. If you're ready a configuration file from a user and only different subsets of them are valid, somewhere in your code you still need all of the complexity to let the user know they have passed in an invalid combination. And all of that logic is equally or more complex than an enum.

If all of those cases can be handled by a few simple "if" in your code, then you'll have only a few valid options in your enum. If you have a ton of valid options you need to list in your enum, then you'll have a tone of cases you need to handle in your code.

Your underlying complaint to me sounds like you've received a lot of complex cases via your requirements. But either way the complexity is there in your code regardless of whether you validate it upfront in an enum, or deep in our code base.

Post reply on HN