Imagine you've implemented a large program in a purely functional way. All the
data is properly threaded in and out of functions, and there are no truly
destructive updates to speak of. Now pick the two lowest-level and most
isolated functions in the entire codebase. They're used all over the place,
but are never called from the same modules. Now make these dependent on each
other: function A behaves differently depending on the number of times
function B has been called and vice-versa.
That sounds like a terrible design decision.
Having two functions which are used all over the place by different modules, and then linking their behaviour to some piece of state that wasn’t passed into either of them is a terrible idea. Suddenly that state becomes an implicit argument AND return value, and I have to know about that before I can call the function… Not only that, now I’ve lost my ability to reason about the behaviour of the function independently from the execution of the program. Further, functions A and B are now impossible to test in isolation. I need to actually call A multiple times before I can test B properly. Then once I’ve done that, well I’ve already called B a few times, so how do I test A? I’d have to have assertions for the behaviour of A and B tangled up in the same tests…
This is a mess, whatever programming language/paradigm you prefer.
That's also sort of missing the entire goddamn point of functional programming. The point of functional programming is that functions produce the exact same thing every time. That's why you don't have to evaluate them until the end value in the chain is needed, and why all state is nicely encapsulated elsewhere (e.g. in IO monad). If that state changes, the entire evaluation that leads to the final result needs to be re-done. It may represent a part of the overall evaluation. There are no ifs and buts here, even if you need a random number generator, you deal with it the exact same way.
No, the example is unmotivated, but not unreasonable.
For example, say the two functions are computationally expensive, and you want to make them faster. The "piece of state" is a shared cache, i.e. memoization. Furthermore, there's reason to believe that if function A is called with value X, then function B will not be called with Y anytime soon. So if A(X) is called, Y(B) ought to be evicted from the cache.
This is awkward to implement in pure functional languages, but it's not a terrible idea. You can still reason about the functions, test them in isolation, etc.
Your example is very different. It seems you're still describing a pure API: testing those new function implementations can be done with the same test suite used for the slower variants.
In the blog post the author choose to specify that the observable behaviour is not pure.
Did he? The author specified that the function "behaves differently;" to my reading this doesn't require that it be impure. A memoized function behaves differently than the underlying function (with observable consequences, like performance), but still obeys referential transparency, etc.
When you do this, the type system should reflect the fact that function has changed it's signature from `A => B` to `A => MaybeDoesntReturnOrHasLatency[B]`. Then the compiler will force downstream clients to take this into account, preventing weird hard to debug errors later on.
FWIW, I'm not arguing about should or shouldn't - I'm just strengthening the original argument.
That said, I don't think small bounded latency is a good thing to encode in a typesystem, much as I don't think runtimes should be encoded in a typesystem. It gives too much inflexibility for too little gain.
I have wanted small bounded latency to be encoded before. (I'd also want the ability to ignore that information in most other programs I worked on. With the right sort of type system, you don't have inflexibility.)
I guess I should clarify and say "What sort of a cache would use IO and make a pure function impure?". Even if the cache is so large that it doesn't fit in memory, its behaviour would still be referentially transparent.
That's easy to implement in Haskell using unsafePerformIO and it's only "unsafe" in the sense that normal code in a non-purely functional language is unsafe. It's only awkward if the impure behavior is observable.
This is a simplified example derived from one that the author apparently got from an older discussion started by Peter Van Roy [1] that both of them were involved in (and which I happened to read back in the day).
The argument here is about modularity, namely encapsulating internal state within a module without exposing that state to the outside. (For example, so that one can introduce a module-internal auditing mechanism or a cache without altering the module's API.)
Peter Van Roy, of course, is one of the principal authors of Oz, so he may have some natural bias, but I've always found the argument persuasive.
For testing and validation purposes, one will obviously need to use appropriate techniques (e.g. specifying and validating state invariants).
> This is a mess, whatever programming language/paradigm you prefer.
You will of course refactor the code.
The point is - it's harder to make a quick and dirty prototype to see if it's better than the previous version (very common in game programming - you don't have strict requirements, you iterate quickly and check "is this fun yet").
And elegant functional code changes more than elegant imperative code for the same change in requirements, so the refactoring would be bigger for functional code (on the other hand the functional code is probably shorter).
The author never said it was a good design decision, or even a desirable one. He only said it would be impossibly difficult to do in a purely functional program, which is worth considering. Whether or not it's a good design decision is irrelevant.
He's making the argument that functional programming is bad. If it were a terrible design decision then the language precluding its implementation (as he claims functional languages do) would be a positive or at least not a negative. So, he must be making the argument that it is a good thing, or at least a necessary thing.
Even so, the ML's all support this type of behaviour (with globals).
In the same way, we do not criticise functional languages for not allowing goto style control flow.
It is very relevant, because author tries to paint this difficulty as a bad thing. It isn't. If the language, or a tool, makes it hard to do bad things and makes it easy to do good things, it's good.
Well, they're easy, convenient, and have subtle downsides: difficult to test code that depends on them without machinery to reset the seed, potential problems in a multithreaded environment, may be used in ways that do not produce evenly distributed outcomes, and for CSPRNGs a whole other bunch of crypto downsides.
Banning RNGs from e.g. avionics control systems sounds more like a good decision than a bad one.
Why yes, yes, of course. I fail to see where avionics need PRNGs (but I don't program avionics, so I might miss some uses). But surely UUIDs are as bad as random numbers in software for a plane?
Having two functions which are used all over the place by different modules, and then linking their behaviour to some piece of state that wasn’t passed into either of them is a terrible idea. Suddenly that state becomes an implicit argument AND return value, and I have to know about that before I can call the function… Not only that, now I’ve lost my ability to reason about the behaviour of the function independently from the execution of the program. Further, functions A and B are now impossible to test in isolation. I need to actually call A multiple times before I can test B properly. Then once I’ve done that, well I’ve already called B a few times, so how do I test A? I’d have to have assertions for the behaviour of A and B tangled up in the same tests…
This is a mess, whatever programming language/paradigm you prefer.