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.
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.
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.
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.
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.)
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.
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 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).
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?
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).
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.
In C, this is easy! It can be done quickly and cleanly by adding some global
variables. In purely functional code, this is somewhere between a major
rearchitecting of the data flow and hopeless.*
That's the actual problem with C -- people can (and therefore they do) make remote parts of the program depend on each other. One global is fine, but then comes another one, ... if there are two very remote, hidden functions that suddenly need to depend on each other then yes, it's OK if this is hard because you are fundamentally changing information flow in your system.
> In C, this is easy! It can be done quickly and cleanly by adding some global variables. In purely functional code, this is somewhere between a major rearchitecting of the data flow and hopeless.
"Cleanly" and "global variables" never mesh.
> Here's a simple statement:
if (a > 0) {
a++;
}
> In single-assignment form a new variable is introduced to avoid modifying an existing variable, and the result is rather Erlangy:
if (a > 0) {
a1 = a + 1;
} else {
a1 = a;
}
Er. Plenty of functional languages will let you shadow existing bindings:
let a = if a > 0 then a+1 else a
However, if you held a reference to 'a' before the shadowing, its value will not change.
"Imagine you've implemented world peace. Now let's try to drop couple of nukes in the middle of this..."
Anyway, the point of functional programming is to have as many "immovable parts" as possible, because mutation stands in for parts in motion. And everyone in engineering knows parts that move are usually the first point of failure.
You might need a moving part here or there in a usable design eventually (not always) but you should do you darn best to have them isolated and as few as possible.
Aha! Moving parts are failure-prone because of friction. And friction happens when something's changing under you. Which means mutation. Nice analogy ;-)
Functional programming has some serious problems, but that it doesn't allow quickly changing behaviour by introducing global state isn't one of them (I'd say its one of the features).
It takes a whole lot less mental effort to iteratively produce a bad solution in OO, than a good solution in FP.
I don't have anything to back this claim up, but to me it seems the "solution space" in FP is a lot smaller than in OO. As soon as you have mutability everywhere, you are just one more if() or more one state away from achieving what you want. In OO there are a million bad solutions to every problem, so the chance that a group of mediocre developers eventually stumble over one of the OO solutions to a problem is pretty good. It's so good that you can almost be sure that given enough time, any group of developers will reach the goal of a working program (for some definition of working). This makes it appealing to business.
Having recently completed my first (small) Clojure project for a client, I don't think the solution space is small unless you're talking about a pretty constrained problem. Given a broad enough scope, there are many approaches/solutions in FP that can yield similar results.
Clojure strikes an impressive balance between functional purity (which I'll agree isn't always the most natural solution) and mutability. You can write purely functional programs, but you can also carefully introduce state and mutability in a controlled manner.
Yeah, purely functional programming can approach an academic exercise. Real-life programs are different, the hosting of mutable state is inherent in many tasks and programs, and cannot be avoided so it makes no sense to trick the language into doing something that it can't easily do.
But functional programming doesn't need to mean all purely functional programming. Functional programming is easy to reason about but it can only be useful as long as it's only applied to the part of the program for which it is a fit. That's quite like how object-oriented programming is a perfect fit for programming user interfaces but not necessarily to managing a data flow.
Write as much in functional style as possible. That will form a nice set of tools upon which you can build. If you need to modify state, you might be able to parameterize a pure function and handle the updates outside but if things get too complex, just pass in a struct or a dict. You can always refactor the function into smaller functions that are, again, purely functions. It's just a matter of drawing the line where it's simplest.
Just as it doesn't make sense to write a program using only global state and gotos it doesn't make sense to write a purely functional program. (And it doesn't make sense to write a program using only objects, either. I'm looking at you, Java.)
There needs to be an interface between the functional code and the code that has side-effects. I like Clojure there a lot. It supports mutable state but makes access to it explicit and controlled. It also support writing a function that looks pure but internally uses a temporary mutable store for data crunching, which is then explicitly finalized before it's returned back as an immutable piece of data.
1 - You don't know what's happening. Even with higher level imperative languages you have a (direct) correspondance between code and machine code (or something similar like bytecode). Not in functional
2 - Sometimes you want side-effects, in ways you didn't know they were side effects (like writing/reading a file, random numbers, etc). Now Haskell's way of solving this may be mathematically elegant, but it's clunky in real life.
3 - Doing things functionally require a different mindset. You think of a for/if cond() break, and you need to rethink this on functional (and a lot of other things).
> 1 - You don't know what's happening. Even with higher level imperative languages you have a (direct) correspondance between code and machine code (or something similar like bytecode). Not in functional
There is a direct correspondance between Haskell code and machine code. GHC translates from Haskell into an imperative language called C--, which is basically a restricted subset of C. Sure, the translation is of a different style than you are used to in the imperative world, but it's still a direct translation.
> 2 - Sometimes you want side-effects, in ways you didn't know they were side effects (like writing/reading a file, random numbers, etc). Now Haskell's way of solving this may be mathematically elegant, but it's clunky in real life.
Arguably, but after several years with Haskell I now find unrestricted side-effects "clunky" and effect typing much more practical.
> 3 - Doing things functionally require a different mindset. You think of a for/if cond() break, and you need to rethink this on functional (and a lot of other things).
I agree. It requires a significant change to your way of thinking about programming.
Well, all languages translate to machine code eventually, but that doesn't make all languages equally easy to understand.
Specifically, some people try to understand code by mentally stepping through it. IMO that's a lot easier in imperative languages than in Haskell. In imperative languages, you step down one line at a time, and loop back in easily specified places. In Haskell, on the other hand, you need to imagine graph reduction in your head. That's already pretty hard for most people, but it gets worse! You can't mentally step through a Haskell function in isolation, because the sequence of graph reduction steps depends on what the caller will do with the answer.
That's not just a theoretical difficulty. Take a look at this thread: http://lambda-the-ultimate.org/node/3127. A bunch of programming language researchers and long-time functional programmers are looking at a six-line Haskell program implementing the Sieve of Eratosthenes, and cannot figure out its big-O complexity. I can't, either.
Maybe I am splitting hairs then. I still hold that the translation is fairly simple. On the other hand, the operational steps that the machine code goes through can be mind bending, and I still have trouble figuring it out myself. Perhaps that's what
raverbashing meant all along.
Can you really map gcc output to your C code ? With all the optimizations (CF, CSE, RA, ...) the assembly code can vary dramatically (from what I recall from compilation classes at least)
This sort of thing stems from fundamentally different ways of thinking about the task of programming, depending on what kind of problems you're tying to solve. Let me sketch some categories of programmer:
Mathematician: primarily concerned with numbers and the stateless universe of mathematical objects. Primary audience for functional programming.
Roboticist: because "control systems engineer" doesn't trip off the tongue. Primarily concerned with physical machinery. Trained on things like Kalman filters and lift controller state machines. Would like the certainty of functional programming but tends to write sphagetti assembler instead in order to reduce interrupt latency.
Plumber: someone who would like to build a system by joining together parts. Perpetually disappointed by how hard this is. Labview is the archetype system here, although a lot of web framework/"stack" talk shows this kind of thinking.
Bureaucrat: someone for whom the records are the primary concern. Tends to build systems which look like CRUD but actually embed a complex workflow which is divided between humans and computers.
The thing is, only the mathematicians are really comfortable hiding all the state in the world behind monads. "Plumbers" might be the next easily persuadable, as FRP maps quite well to this way of thinking, as does map/reduce. But there isn't quite the tooling to enable building software out of pieces joined by pipes.
In the electronic and bureaucratic control systems, changing state is the point of the program. The thinking revolves around state, when it should be updated, and what it should be updated to. Stuffing this all behind a monad results in unnatural-feeling programs. There's a hybrid point where you have a "state" part of the program and a pile of "next state" logic which is basically the only way to write Verilog/VHDL - and it's hard to learn and not so naturally expressive.
Perhaps the way in is something like C++'s "const": you take a language that isn't functional and mark parts of your code as non-state-modifying, making it easier to subject them to formal reasoning.
> Perhaps the way in is something like C++'s "const": you take a language that isn't functional and mark parts of your code as non-state-modifying, making it easier to subject them to formal reasoning
Or even better, you do the opposite: non-mutable is the default case, and you use special keywords when it's not.
I appreciate this kind of distinction. However, I wouldn't be so quick to say that purely functional state transitions are unnatural or difficult. If you just formulate it slightly differently, it makes lots of sense: the point of the program is to calculate the next state given a current state and some input. And that's natural to formulate as a pure function of type (Input, State) → State.
Let's also not overestimate the elegance, naturalness, or correctness of the typical imperative approaches to control systems... Most programs are already difficult to understand, think about, predict, and change. And a huge reason is that they tend to lack principles. How often do you look at code and feel like there's no reason to believe that it works correctly, other than the fact that it seems to? And then you look at the growing bug count in the issue tracker...
State monads are (from one perspective) an implementation pattern used in Haskell to encapsulate state transitions. They're not necessary, and actually they're mostly used because they enable some syntactic sugar.
I've written very stateful code in Haskell, professionally, and it's been quite nice. No worse than any other language, and nicer in many ways. STM and light-weight threads go a long way.
I wish I had an example of a well-designed CRUD-ish system in Haskell to point to. Maybe there are some. I haven't had the opportunity to make one, but I feel confident that some very nice design patterns could emerge.
To give some indication that semi-nice stateful things can be written in Haskell in natural styles, here is a very simple component of an "IRC relay" system I never quite finished. [1] It's an independent process that reads IRC messages from a RabbitMQ queue, and writes "PONG" for every "PING". It's based around the `pipes` library and as you can see the basic effect of the program is explicitly stated in the main function.
In the electronic and bureaucratic control systems, changing state is the
point of the program. The thinking revolves around state, when it should be
updated, and what it should be updated to. Stuffing this all behind a monad
results in unnatural-feeling programs.
Changing state is the point of almost any long-running user-facing application. And you are correct, thinking about when state should be updated and what it should be updated to is very important. So important in-fact, that allowing state updates to occur anywere in the program, with no rhyme nor reason, dramatically reduces an engineer’s ability to understand that program.
It may feel natural, or familiar, or easy to change state wherever it is convenient to do so. But that doesn’t make it a good engineering decision. As I mentioned in another comment it introduces complexity around logical reasoning, future change, code re-use, and testing.
It is much simpler if you define your application’s state in one place, and in terms of the actions that can be performed to update the state. This is simple, and easy to reason about. Then the functions to update the state can be pure. Again, this is simple, and easy to reason about.
> You could pass a global state in and out of every function in your program, but why not make that implicit?
Because making it explicit allows you to reason about what a piece of code does without inspecting every little part of it for the global state it touches.
I'm not sure where you get the idea that much would need rewriting, but if anything does, the advantages of having modular code that you can reason about are worth it.
If you read what I quoted again, he is basically arguing for the use of plain old imperative code over OOP. It's widely considered bad practice to use global state at all in OOP languages, except where strictly necesssary for handling specific resources anyway.
OOP languages still have global variables via "static", but good OOP code design will try to avoid static entirely, and pass any global state explicitly down through functions and classes all the way from main. The reason is simple: It allows you to reason about what every part of code does. The only problem is that you can't reason about some code someone else wrote, as it may use static internally.
And this is really what FP intends to fix. Instead of just "trying" to avoid static, forbid it entirely. Now you can take some code written by anyone and be sure that it isn't going to interfere with another piece of code you write separately. Any time you explicitly want parts of code to interact in such ways, making those interactions explicit still allows you to reason about their collective behavior (and isolate that behaviour from the rest of your code).
Hague suggests that with FP, you need to write in SSA form whenever you're passing state around explicitly, but this argument is nonsense - FP has all the tools to avoid the use of assignment at all. Control flow is abstracted away as functions and they have useful names (or operators). What makes Hague's suggestion most absurd though is his use of an example from imperative languages, where he uses an if STATEMENT. Of course, you probably do need to use variable assignment if your language prevents you treating constructs as expressions.
Sometimes it's worth it, sometimes it's not. Exploratory programming is a thing. Example:
Spec 1.0 update position of objects according to velocity
for in imperative, map in FP
Spec 1.1 what if we made the objects that are far from the center of the group move towards the center?
for followed by for in IP, reduce and map in FP
Spec 1.2 scratch that, just remove the objects that are far from the center
for and for in IP, reduce, map, filter in FP
Spec 1.3 and spawn explosions around them
add 1 call in ip, refactoring required in fp
Spec 1.4 also they should play explosion.mp3
add another call in IP, another refactoring required in FP
Spec 1.5 this should happen for all destroyed objects
move both calls to function that removes object in IP, another refactoring (with passing a lot of state in new ways) in FP
And so on.
After you're OK with the result you can make the code nice in IP too. And you can do this once, instead of doing this every time anything changes.
It's like a text editor that forbids you to have temporarily non compiling code. Only refactorings are available, you can't write if by hand. Pain in the ass.
Yes I know about paredit, people still copy paste, and write parents by hand.
It'd be nice if 1.3 and 1.4 could be done with just 1 line of call, but you're missing the fact that pretty much any graphics and audio library will have some "context" singleton through which it is called due to the way these interact with the operating system. The APIs are deliberately designed this way to encapsulate the global state they use.
Sure you can make a global variable for the context and avoid passing it around - but the refactoring route is the preferred option in the imperative world anyway.
If you're writing in Haskell with any expectation of using IO for some exploratory programming like this, the obvious solution is to make your functions of type (:: MonadIO m => ... -> m ...), then any refactoring you need to do to add additional facets to your state can be specified elsewhere as part of a MonadIO transformer. Your calls to "play explosion.mp3" and "render explosion" just need a liftIO on them.
When functional code gets unwieldy, it's probably time to place an object layer above it. You see the model in Erlang (where processes act as objects) and the messsaging middleware of many IT systems.
It's time for this terrible "Objects in the large, functions in the small" idea to be put to bed.
FP can do side-effects, asynchrony etc just fine, it's just about controlling them so you can still reason about your program and compose its elements easily.
Kay-style message-sending object graphs are neither modular nor composable; state & effects leak transitively by observation, and they cannot be recombined in self-similar patterns. Local reasoning is impossible.
We can talk about specific situations where say, Actors models might have pros and cons, but a blanket recommendation that "it's probably time for an object layer" is rubbish.
Why not design for composability and modularity top to bottom?
54 comments
[ 2.4 ms ] story [ 132 ms ] threadHaving 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.
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.
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.
http://blog.codinghorror.com/falling-into-the-pit-of-success...
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.
In the blog post the author choose to specify that the observable behaviour is not pure.
Does a language like Haskell give you the choice? If so,would it be idiomatic?
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.
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).
Banning RNGs from e.g. avionics control systems sounds more like a good decision than a bad one.
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).
[1] http://lambda-the-ultimate.org/classic/message9361.html
Isin't it the other way round?
"Cleanly" and "global variables" never mesh.
> Here's a simple statement:
> In single-assignment form a new variable is introduced to avoid modifying an existing variable, and the result is rather Erlangy: Er. Plenty of functional languages will let you shadow existing bindings: However, if you held a reference to 'a' before the shadowing, its value will not change.Anyway, the point of functional programming is to have as many "immovable parts" as possible, because mutation stands in for parts in motion. And everyone in engineering knows parts that move are usually the first point of failure.
You might need a moving part here or there in a usable design eventually (not always) but you should do you darn best to have them isolated and as few as possible.
It takes a whole lot less mental effort to iteratively produce a bad solution in OO, than a good solution in FP.
I don't have anything to back this claim up, but to me it seems the "solution space" in FP is a lot smaller than in OO. As soon as you have mutability everywhere, you are just one more if() or more one state away from achieving what you want. In OO there are a million bad solutions to every problem, so the chance that a group of mediocre developers eventually stumble over one of the OO solutions to a problem is pretty good. It's so good that you can almost be sure that given enough time, any group of developers will reach the goal of a working program (for some definition of working). This makes it appealing to business.
But functional programming doesn't need to mean all purely functional programming. Functional programming is easy to reason about but it can only be useful as long as it's only applied to the part of the program for which it is a fit. That's quite like how object-oriented programming is a perfect fit for programming user interfaces but not necessarily to managing a data flow.
Write as much in functional style as possible. That will form a nice set of tools upon which you can build. If you need to modify state, you might be able to parameterize a pure function and handle the updates outside but if things get too complex, just pass in a struct or a dict. You can always refactor the function into smaller functions that are, again, purely functions. It's just a matter of drawing the line where it's simplest.
Just as it doesn't make sense to write a program using only global state and gotos it doesn't make sense to write a purely functional program. (And it doesn't make sense to write a program using only objects, either. I'm looking at you, Java.)
There needs to be an interface between the functional code and the code that has side-effects. I like Clojure there a lot. It supports mutable state but makes access to it explicit and controlled. It also support writing a function that looks pure but internally uses a temporary mutable store for data crunching, which is then explicitly finalized before it's returned back as an immutable piece of data.
Yeah, like some kind of IO type or context. Gosh, someone should try and invent that sometime. :-)
1 - You don't know what's happening. Even with higher level imperative languages you have a (direct) correspondance between code and machine code (or something similar like bytecode). Not in functional
2 - Sometimes you want side-effects, in ways you didn't know they were side effects (like writing/reading a file, random numbers, etc). Now Haskell's way of solving this may be mathematically elegant, but it's clunky in real life.
3 - Doing things functionally require a different mindset. You think of a for/if cond() break, and you need to rethink this on functional (and a lot of other things).
There is a direct correspondance between Haskell code and machine code. GHC translates from Haskell into an imperative language called C--, which is basically a restricted subset of C. Sure, the translation is of a different style than you are used to in the imperative world, but it's still a direct translation.
> 2 - Sometimes you want side-effects, in ways you didn't know they were side effects (like writing/reading a file, random numbers, etc). Now Haskell's way of solving this may be mathematically elegant, but it's clunky in real life.
Arguably, but after several years with Haskell I now find unrestricted side-effects "clunky" and effect typing much more practical.
> 3 - Doing things functionally require a different mindset. You think of a for/if cond() break, and you need to rethink this on functional (and a lot of other things).
I agree. It requires a significant change to your way of thinking about programming.
Specifically, some people try to understand code by mentally stepping through it. IMO that's a lot easier in imperative languages than in Haskell. In imperative languages, you step down one line at a time, and loop back in easily specified places. In Haskell, on the other hand, you need to imagine graph reduction in your head. That's already pretty hard for most people, but it gets worse! You can't mentally step through a Haskell function in isolation, because the sequence of graph reduction steps depends on what the caller will do with the answer.
That's not just a theoretical difficulty. Take a look at this thread: http://lambda-the-ultimate.org/node/3127. A bunch of programming language researchers and long-time functional programmers are looking at a six-line Haskell program implementing the Sieve of Eratosthenes, and cannot figure out its big-O complexity. I can't, either.
Mathematician: primarily concerned with numbers and the stateless universe of mathematical objects. Primary audience for functional programming.
Roboticist: because "control systems engineer" doesn't trip off the tongue. Primarily concerned with physical machinery. Trained on things like Kalman filters and lift controller state machines. Would like the certainty of functional programming but tends to write sphagetti assembler instead in order to reduce interrupt latency.
Plumber: someone who would like to build a system by joining together parts. Perpetually disappointed by how hard this is. Labview is the archetype system here, although a lot of web framework/"stack" talk shows this kind of thinking.
Bureaucrat: someone for whom the records are the primary concern. Tends to build systems which look like CRUD but actually embed a complex workflow which is divided between humans and computers.
The thing is, only the mathematicians are really comfortable hiding all the state in the world behind monads. "Plumbers" might be the next easily persuadable, as FRP maps quite well to this way of thinking, as does map/reduce. But there isn't quite the tooling to enable building software out of pieces joined by pipes.
In the electronic and bureaucratic control systems, changing state is the point of the program. The thinking revolves around state, when it should be updated, and what it should be updated to. Stuffing this all behind a monad results in unnatural-feeling programs. There's a hybrid point where you have a "state" part of the program and a pile of "next state" logic which is basically the only way to write Verilog/VHDL - and it's hard to learn and not so naturally expressive.
Perhaps the way in is something like C++'s "const": you take a language that isn't functional and mark parts of your code as non-state-modifying, making it easier to subject them to formal reasoning.
Or even better, you do the opposite: non-mutable is the default case, and you use special keywords when it's not.
Let's also not overestimate the elegance, naturalness, or correctness of the typical imperative approaches to control systems... Most programs are already difficult to understand, think about, predict, and change. And a huge reason is that they tend to lack principles. How often do you look at code and feel like there's no reason to believe that it works correctly, other than the fact that it seems to? And then you look at the growing bug count in the issue tracker...
State monads are (from one perspective) an implementation pattern used in Haskell to encapsulate state transitions. They're not necessary, and actually they're mostly used because they enable some syntactic sugar.
I've written very stateful code in Haskell, professionally, and it's been quite nice. No worse than any other language, and nicer in many ways. STM and light-weight threads go a long way.
I wish I had an example of a well-designed CRUD-ish system in Haskell to point to. Maybe there are some. I haven't had the opportunity to make one, but I feel confident that some very nice design patterns could emerge.
To give some indication that semi-nice stateful things can be written in Haskell in natural styles, here is a very simple component of an "IRC relay" system I never quite finished. [1] It's an independent process that reads IRC messages from a RabbitMQ queue, and writes "PONG" for every "PING". It's based around the `pipes` library and as you can see the basic effect of the program is explicitly stated in the main function.
[1]: https://github.com/mbrock/klatch/blob/master/Klatch/Ponger/P...
It may feel natural, or familiar, or easy to change state wherever it is convenient to do so. But that doesn’t make it a good engineering decision. As I mentioned in another comment it introduces complexity around logical reasoning, future change, code re-use, and testing.
It is much simpler if you define your application’s state in one place, and in terms of the actions that can be performed to update the state. This is simple, and easy to reason about. Then the functions to update the state can be pure. Again, this is simple, and easy to reason about.
Because making it explicit allows you to reason about what a piece of code does without inspecting every little part of it for the global state it touches.
If you don't know the exact control flow when you start it's a lof of busywork to modify it each time you change your mind.
If you read what I quoted again, he is basically arguing for the use of plain old imperative code over OOP. It's widely considered bad practice to use global state at all in OOP languages, except where strictly necesssary for handling specific resources anyway.
OOP languages still have global variables via "static", but good OOP code design will try to avoid static entirely, and pass any global state explicitly down through functions and classes all the way from main. The reason is simple: It allows you to reason about what every part of code does. The only problem is that you can't reason about some code someone else wrote, as it may use static internally.
And this is really what FP intends to fix. Instead of just "trying" to avoid static, forbid it entirely. Now you can take some code written by anyone and be sure that it isn't going to interfere with another piece of code you write separately. Any time you explicitly want parts of code to interact in such ways, making those interactions explicit still allows you to reason about their collective behavior (and isolate that behaviour from the rest of your code).
Hague suggests that with FP, you need to write in SSA form whenever you're passing state around explicitly, but this argument is nonsense - FP has all the tools to avoid the use of assignment at all. Control flow is abstracted away as functions and they have useful names (or operators). What makes Hague's suggestion most absurd though is his use of an example from imperative languages, where he uses an if STATEMENT. Of course, you probably do need to use variable assignment if your language prevents you treating constructs as expressions.
Spec 1.0 update position of objects according to velocity
Spec 1.1 what if we made the objects that are far from the center of the group move towards the center? Spec 1.2 scratch that, just remove the objects that are far from the center Spec 1.3 and spawn explosions around them Spec 1.4 also they should play explosion.mp3 Spec 1.5 this should happen for all destroyed objects And so on.After you're OK with the result you can make the code nice in IP too. And you can do this once, instead of doing this every time anything changes.
It's like a text editor that forbids you to have temporarily non compiling code. Only refactorings are available, you can't write if by hand. Pain in the ass.
Yes I know about paredit, people still copy paste, and write parents by hand.
Sure you can make a global variable for the context and avoid passing it around - but the refactoring route is the preferred option in the imperative world anyway.
If you're writing in Haskell with any expectation of using IO for some exploratory programming like this, the obvious solution is to make your functions of type (:: MonadIO m => ... -> m ...), then any refactoring you need to do to add additional facets to your state can be specified elsewhere as part of a MonadIO transformer. Your calls to "play explosion.mp3" and "render explosion" just need a liftIO on them.
Tell Above, and Ask Below.
https://news.ycombinator.com/item?id=3876136
When functional code gets unwieldy, it's probably time to place an object layer above it. You see the model in Erlang (where processes act as objects) and the messsaging middleware of many IT systems.
FP can do side-effects, asynchrony etc just fine, it's just about controlling them so you can still reason about your program and compose its elements easily.
Kay-style message-sending object graphs are neither modular nor composable; state & effects leak transitively by observation, and they cannot be recombined in self-similar patterns. Local reasoning is impossible.
We can talk about specific situations where say, Actors models might have pros and cons, but a blanket recommendation that "it's probably time for an object layer" is rubbish.
Why not design for composability and modularity top to bottom?