This article seems to conflate strong type systems with functional programming, except in point 8. It makes sense why- OCaml and Haskell are functional and were early proponents of these type systems. But, languages like Racket don’t have these type systems and the article doesn’t do anything to explain why they are _also_ better for reliability.
The term "functional programming" is so ill-defined as to be effectively useless in any kind of serious conversation. I'm not aware of any broadly accepted consensus definition. Sometimes people want to use this category to talk about purity and control of side effects and use the term "functional programming" to refer to that. I would advocate the more targeted term "pure functional programming" for that definition. But in general I try to avoid the term altogether, and instead talk about specific language features / capabilities.
Once you accept Curry-Howard, untyped FP languages are hard to take seriously as a foundation for reliability. Curry-Howard changes the entire game. FP and strong types were clearly meant for each other.
Untyped FP languages can be productive, flexible, even elegant (I guess) but they are structurally incapable of expressing large classes of correctness claims that typed FP makes routine.
That doesn’t make them useless, just, you know. Inferior.
>In banking, telecom, and payments, reliability is not a nice to have. It is table stakes.
This reliability isn't done by being perfect 100% of the time. Things like being able to handle states where transactions don't line up allowing for payments to eventually be settled. Or for telecom allowing for single parts of the system to not take down the whole thing or adding redundancy. Essentially these types of businesses require fault tolerance to be supported. The real world is messy, there is always going to be faults, so investing heavily into correctness may not be worth it compared to investing into fault tollerance.
I like good type systems, too, but they won't save you from bugs that are better addressed by fuzz testing, fault injection testing and adversarial mindset shifts.
Functional programming: no, functional programming as in: the final program consists in piping functions together and calling the pipe. In my opinion, that tends to get in the way of complex error handling.
The problem being that raising Exceptions at a deep level and catching them at some higher level is not pure functional programming. So your code has to deal with all the cases. It is more reliable if you can do it, but large systems have way too many failure points to be able to handle them all in a way that is practical.
I think there is a strong case that ADTs (algebraic data types) aren't so great after all. Specifically, the "tagged" unions of ADT languages like Haskell are arguably pretty clearly inferior to the "untagged" unions of TypeScript or Scala 3. Because the latter actually behave like a logical "or" rather than an artificial construct that needs to be wrapped and unwrapped.
All the line items are decent things, worth doing, but the claim about how much following the line items would improve reliability is super exaggerated.
> [Most production incidents] are due to the code entering a state that should never have been possible.
I have never seen evidence that this is even remotely true, and I've been looking at software reliability research in the last few months.
Instead, it is more true that most production incidents are due to the system entering into one of thousands of unsafe states which were possible and latent in production potentially for years. In a sufficiently complex system—all interesting and important software projects—functional programming is not strong enough a tool to prevent even a sliver of potential accidents.
> Arguments that these degraded conditions should have been recognized before the overt accident are usually predicated on naïve notions of system performance. System operations are dynamic, with components (organizational, human, technical) failing and being replaced continuously. — https://how.complexsystems.fail/
> ... functional programming and static typing make things more reliable.
> But this isn't a falsifiable claim.
Saying "this isn't falsifiable" is a wild claim. Indeed the claim "functional programming and static typing make things more reliable" is falsifiable, as long as you admit a statistical understanding. The world is messy and experiments have noise, so what would you use if not statistics? Anecdotes?: no. Purely deductive methods?: no; we should not expect any single technique to be a silver bullet.
Good studies and analyses lay out a causal model and use strong methodologies for showing that some factor has a meaningful impact on some metric of interest. I recommend this as a starting point [1]
A few mention on tests, but I expected more. The main value of pure functions is that now their behavior is representative in tests. In fact, I'd argue that all you need for reliability is determinism and tests of all equivalent scenarios. functional programming (and immutability) are only helpful to the extent that it's easier to have representative tests, but not necessarily required.
> In fact, I'd argue that all you need for reliability is determinism and tests of all equivalent scenarios.
Any insights as to how to get effective determinism without pure functions?
Pure functions win here because we only have to reason about the function arguments. Reasoning is easier when you have to less of it! Without pure functions, the state space explodes, because anything anywhere could have a side-effect — madness, I say! — so how would you figure out equivalent states? Special case inspection I suppose?
There is a continuum between "anything goes" and "pure functions". From a certain light, all the varieties of encapsulation can be seen as explorations of this theme. (An idea: perhaps there are languages where side-effects can be circumscribed in ways that don't rely on the high bar of pure functions?)
The arguments against pure functions appear to be somewhat contingent: current adoption levels, practice, convenience, and taste. Things that are contingent have a way of changing over time as new methods emerge and new values take hold.
I don't hold shallow human preferences in high regard. As an example, many humans and organizations in practice trade security for convenience, at significant cost — especially when that cost is external to them.
At the same time, onerous tasks encourage work-arounds. This is the crux of it I think: unless people perceive the value of pure functions (to do what they want to do), some will seek shortcuts. So we need some combination of (1) making them easier to work with; (2) motivating people to aspire to higher principles; namely, high quality software that does not open to door to exploitation, misuse, botnets, etc; and/or (3) internalize the external costs.
As an example of (3), if people and companies faced predictable and commensurate costs when their flawed software inflicts harm on others, they would be more inclined to care about correctness.
These arguments unfortunately fail flat in front of industrial use. AWS could be considered "critical" by most metrics and what is is it written in? Java
Was just talking with someone the other day who used to write Haskell professionally but is now using Python. He said that in his experience when there are bugs the "blast radius" is much larger in a dynamic language like Python than in a static language like Haskell. That has been my experience as well.
Something I haven't seen talked about, though, is how powerful the type system is for constraining LLMs when using them to generate code. I was recently trying to get LLMs to generate code for a pretty vague and complex task in Haskell. I wasn't having much luck until I defined a very clear set of types and organized them into a very clear and constrained interface that I asked the LLM to code to. Then the results were much better!
Sure, you can use these same techniques in less strongly typed languages like Rust, and you can probably also use a similar approach in dynamically typed languages, but Haskell's pure functions allow you to create much stronger guard rails constraining what kinds of code the LLM can write.
Amen. I've been coding a big hobby project in Rust since July, after having spent years using Haskell for such things. I chose Rust because the primary DB I wanted to use (TypeDB) only had drivers for Rust and Python at the time. Rust is popular relative to Haskell, so I thought others might be more likely to sign on, and the type system seemed almost as expressive.
But since purity is not encoded in Rust's type system, any function might do any kind of IO -- in particular, read from or write to disk or one of the DBs. That makes the logic much harder to reason about.
(Also, Rust's syntax is so noisy and verbose that it's harder to see what's going on, and less context fits in my head at one time. I'm getting better at paying that cost, but I wish it weren't there.)
I can't say I made the wrong decision, but I often fantasize about moving most of the logic into Haskell and just calling Rust from Haskell when I need to call TypeDB from Rust.
I've found it useful in limited cases for writing optics which can be incredibly obtuse, sometimes boilerplatey, and yet ultimately accomplish what in other languages might be considered utterly trivial use cases... consider the following prompt and output:
reply with one-line haskell source code ONLY: implement the function projectPair :: (Field1 s s a a, Field2 s s b b) => Lens s s (a, b) (a, b)
lens (\s -> (s^._1, s^._2)) (\s (a,b) -> s & _1 .~ a & _2 .~ b)
... which can be shown to satisfy the three lens laws. If you can understand the types it is generally true that the implementation falls out much more easily, in a similar vein as "show me your tables, and I won't usually need your flowchart; it'll be obvious."
I suppose LLMs are good for this and other extremely constrained forms of boilerplate production. I consider it an incremental improvement over go codegen. Everything else I still tend to hand-write, because I don't consider source code production the bottleneck of software development.
Interesting experience and perhaps not entirely surprising given that type-hole driven development was already a thing prior to LLMs. Like how many ways are there to implement "[a] -> [b] -> [(a,b)]", let alone when you provide a vague description of what that is supposed to do.
See also the type-state pattern. It is commonly used in Rust along with the builder pattern [1]. Quoting:
"""The typestate pattern is an API design pattern that encodes information about an object’s run-time state in its compile-time type. In particular, an API using the typestate pattern will have:
- Operations on an object (such as methods or functions) that are only available when the object is in certain states,
- A way of encoding these states at the type level, such that attempts to use the operations in the wrong state fail to compile,
- State transition operations (methods or functions) that change the type-level state of objects in addition to, or instead of, changing run-time dynamic state, such that the operations in the previous state are no longer possible.
This is useful because:
- It moves certain types of errors from run-time to compile-time, giving programmers faster feedback.
- It interacts nicely with IDEs, which can avoid suggesting operations that are illegal in a certain state.
- It can eliminate run-time checks, making code faster/smaller."""
Some other languages can do it as well: see [2] for a discussion.
Most of the musings on enforcing invalid states with ADTs are impractical when working with real data that needs runtime validation when it enters the system.
Using a schema/spec/type library at runtime that is even more powerful than ADTs is a better investment - or to be less controversial - is an additional investment on top of types.
Yes, it means the compiler can't help you as much, but who has time waiting for a compiler anyways ;)
I find the "make illegal state unrepresentable via types" idea great for software that needs to fly a plane, but for enterprise software not so much. The cost/benefit is not justifiable.
34 comments
[ 2.7 ms ] story [ 60.2 ms ] threadUntyped FP languages can be productive, flexible, even elegant (I guess) but they are structurally incapable of expressing large classes of correctness claims that typed FP makes routine.
That doesn’t make them useless, just, you know. Inferior.
This reliability isn't done by being perfect 100% of the time. Things like being able to handle states where transactions don't line up allowing for payments to eventually be settled. Or for telecom allowing for single parts of the system to not take down the whole thing or adding redundancy. Essentially these types of businesses require fault tolerance to be supported. The real world is messy, there is always going to be faults, so investing heavily into correctness may not be worth it compared to investing into fault tollerance.
Functional programming: no, functional programming as in: the final program consists in piping functions together and calling the pipe. In my opinion, that tends to get in the way of complex error handling.
The problem being that raising Exceptions at a deep level and catching them at some higher level is not pure functional programming. So your code has to deal with all the cases. It is more reliable if you can do it, but large systems have way too many failure points to be able to handle them all in a way that is practical.
Haha as someone who has worked in one of these domains using FP even - I wish the people in charge agreed with you!
Reliability is a cost center and Product-oriented Builders treat it as such.
> [Most production incidents] are due to the code entering a state that should never have been possible.
I have never seen evidence that this is even remotely true, and I've been looking at software reliability research in the last few months.
Instead, it is more true that most production incidents are due to the system entering into one of thousands of unsafe states which were possible and latent in production potentially for years. In a sufficiently complex system—all interesting and important software projects—functional programming is not strong enough a tool to prevent even a sliver of potential accidents.
> Arguments that these degraded conditions should have been recognized before the overt accident are usually predicated on naïve notions of system performance. System operations are dynamic, with components (organizational, human, technical) failing and being replaced continuously. — https://how.complexsystems.fail/
But this isn't a falsifiable claim. We cannot possibly know if this is true or not.
- Not all of banking and telecom use functional programming or even static typing.
- Functional programming often leads to write-only incomprehensible code; the exact opposite of what you need to have a reliable system.
- There's no hard evidence that static typing improves reliability. Only vibes and feels.
In what language?
> But this isn't a falsifiable claim.
Saying "this isn't falsifiable" is a wild claim. Indeed the claim "functional programming and static typing make things more reliable" is falsifiable, as long as you admit a statistical understanding. The world is messy and experiments have noise, so what would you use if not statistics? Anecdotes?: no. Purely deductive methods?: no; we should not expect any single technique to be a silver bullet.
Good studies and analyses lay out a causal model and use strong methodologies for showing that some factor has a meaningful impact on some metric of interest. I recommend this as a starting point [1]
[1]: https://yanirseroussi.com/2016/05/15/diving-deeper-into-caus...
Any insights as to how to get effective determinism without pure functions?
Pure functions win here because we only have to reason about the function arguments. Reasoning is easier when you have to less of it! Without pure functions, the state space explodes, because anything anywhere could have a side-effect — madness, I say! — so how would you figure out equivalent states? Special case inspection I suppose?
There is a continuum between "anything goes" and "pure functions". From a certain light, all the varieties of encapsulation can be seen as explorations of this theme. (An idea: perhaps there are languages where side-effects can be circumscribed in ways that don't rely on the high bar of pure functions?)
The arguments against pure functions appear to be somewhat contingent: current adoption levels, practice, convenience, and taste. Things that are contingent have a way of changing over time as new methods emerge and new values take hold.
I don't hold shallow human preferences in high regard. As an example, many humans and organizations in practice trade security for convenience, at significant cost — especially when that cost is external to them.
At the same time, onerous tasks encourage work-arounds. This is the crux of it I think: unless people perceive the value of pure functions (to do what they want to do), some will seek shortcuts. So we need some combination of (1) making them easier to work with; (2) motivating people to aspire to higher principles; namely, high quality software that does not open to door to exploitation, misuse, botnets, etc; and/or (3) internalize the external costs.
As an example of (3), if people and companies faced predictable and commensurate costs when their flawed software inflicts harm on others, they would be more inclined to care about correctness.
Something I haven't seen talked about, though, is how powerful the type system is for constraining LLMs when using them to generate code. I was recently trying to get LLMs to generate code for a pretty vague and complex task in Haskell. I wasn't having much luck until I defined a very clear set of types and organized them into a very clear and constrained interface that I asked the LLM to code to. Then the results were much better!
Sure, you can use these same techniques in less strongly typed languages like Rust, and you can probably also use a similar approach in dynamically typed languages, but Haskell's pure functions allow you to create much stronger guard rails constraining what kinds of code the LLM can write.
But since purity is not encoded in Rust's type system, any function might do any kind of IO -- in particular, read from or write to disk or one of the DBs. That makes the logic much harder to reason about.
(Also, Rust's syntax is so noisy and verbose that it's harder to see what's going on, and less context fits in my head at one time. I'm getting better at paying that cost, but I wish it weren't there.)
I can't say I made the wrong decision, but I often fantasize about moving most of the logic into Haskell and just calling Rust from Haskell when I need to call TypeDB from Rust.
I suppose LLMs are good for this and other extremely constrained forms of boilerplate production. I consider it an incremental improvement over go codegen. Everything else I still tend to hand-write, because I don't consider source code production the bottleneck of software development.
"""The typestate pattern is an API design pattern that encodes information about an object’s run-time state in its compile-time type. In particular, an API using the typestate pattern will have:
- Operations on an object (such as methods or functions) that are only available when the object is in certain states,
- A way of encoding these states at the type level, such that attempts to use the operations in the wrong state fail to compile,
- State transition operations (methods or functions) that change the type-level state of objects in addition to, or instead of, changing run-time dynamic state, such that the operations in the previous state are no longer possible.
This is useful because:
- It moves certain types of errors from run-time to compile-time, giving programmers faster feedback.
- It interacts nicely with IDEs, which can avoid suggesting operations that are illegal in a certain state.
- It can eliminate run-time checks, making code faster/smaller."""
Some other languages can do it as well: see [2] for a discussion.
[1]: https://cliffle.com/blog/rust-typestate/
[2]: https://www.reddit.com/r/rust/comments/17l8eez/is_there_any_...
Using a schema/spec/type library at runtime that is even more powerful than ADTs is a better investment - or to be less controversial - is an additional investment on top of types.
Yes, it means the compiler can't help you as much, but who has time waiting for a compiler anyways ;)
I find the "make illegal state unrepresentable via types" idea great for software that needs to fly a plane, but for enterprise software not so much. The cost/benefit is not justifiable.