Hacker News new | ask | show | jobs
by tikhonj 4607 days ago
This is a gross mischaracterization of functional programming and basically attacks a straw man--one that's lamentably common when talking about functional programming.

I'm going to repost a comment I wrote on the blog. It's long and really needs editing, but I hope it gets my thoughts across. I think the part about OOP is also misguided, but it's so obviously tacked on to a rant about functional programming that I just ignored it.

Haskell’s “purity” is not about getting rid of side-effects but about controlling side-effects. It’s making side-effects first-class citizens: now you can write code that talks about having or not having them!

You can still have side-effects however you want, you just have to be explicit about it. To some extent, the fact that this uses monads is incidental: all that’s important is that there is some IO type, some ST type and so on–the fact that they all form monads is almost an implementation detail. That’s why some of the most exciting Haskell features–STM, the IO manager and so on–are all about effects. Clearly, Haskell more than acknowledges effects, so the entire diatribe about ignoring the existence of side-effects is attacking a straw man.

Besides, you can’t simply replace a first-class system for managing effects with static analysis, without essentially reproducing the same restrictions. How would you do something like Haskell’s deterministic parallelism model or reliable STM or the very aggressive loop fusion (and general rewriting) Haskell uses? There’s a reason you don’t see these things done nearly as well in any other languages: all of these fall apart as soon as you introduce side-effects, so you need some way to help the programmer ensure things like this are only used safely.

And this is exactly how types like IO and ST help make code safer. Sure, within an ST block, you have stateful code that’s just as hard to analyze. But you can guarantee that this does not leak outside the block. Similarly, functions can rely on their inputs not doing anything untoward however they’re used. This allows you to explicitly state the assumptions about your inputs: how is this a bad thing? In turn, this makes writing code that takes advantage of these properties much easier: you can ask that your inputs do not cause side-effects in a way that’s self-documenting and easy to verify. Then you’re free to re-evaluate your inputs or call functions however many times you want, as concurrently as you want. At the extreme, this can even be used for security purposes: see Safe Haskell.

Sure you can write pure functions in any language. And you can write side-effecting procedures in Haskell too. But the difference is that Haskell lets you be explicit about whether you want side-effects or not–it’s just another part of your interface. And this is how types like IO and ST help make your code easier to think about: any code that is not in a type like IO or ST can only depend on its arguments, making all the dependencies more explicit. (Note, again, how this is all independent of “monads”–it’s all about effects, and the types just happen to form monads.) This does not make static analysis too much easier, but that was never the point–the goal is to make the code easier to think about, and knowing that there are no hidden state dependencies certainly does that. A static analyzer can follow data flow easily, but it requires quite a bit of thinking for the programmer to do the same!

The core motivation for managing effects à la Haskell is not “mathematical purity”: it’s software engineering. We want code that is easier to think about, has better guarantees and is more modular. The goal is to make code less complex by removing hidden dependencies between distant parts of your program (mutable state) and the effect of evaluation on the meaning of your program (side-effects in general). You can refactor and move around most Haskell code without worrying about breaking its surroundings because any dependencies are explicit. You can extract something into a function, consolidate multiple function calls into one or split one into multiple and generally change your code up quite a bit without worry–these actions cannot change the code’s correctness because effects are managed for you.

Ultimately, functional programming like Haskell is not just normal programming with side-effects outlawed. Instead, it’s a different basis for programming which allows you to manage side-effects explicitly. In this light, papers like “solved problem but with monads” are entirely reasonable: they’re about bringing things over to this new basis. And this goes the other way too: there’s a reason why you don’t see good STM, deterministic parallelism, stream fusion (and rewrite-rule optimizations in general), anything like DPH and anything like Safe Haskell in other languages.

5 comments

The way Haskell manages side effects is inspired, and I don't think anyone can say that Haskell is not a beautiful and coherent language. I only dabbled in Haskell a bit so I may be wrong, but I think that the problem with the way it manages side effects is that it does so through lazy evaluation, and lazy evaluation is hard to wrap your head around.

So I think that if there is one big problem with Haskell, it is this: you say Haskell is about "software engineering". Indeed, the guarantees it provides may assist with software engineering, but those guarantees aren't free. They require programmers to think and program solely within Haskell's lambda calculus and lazy evaluation framework, which is neither the way people think nor the way computers do (the latter is important when doing performance analysis on your code). This constrained framework also takes its toll on software engineering, then. The question is, therefore, when are the guarantees provided by Haskell worth their price? I think there are cases where they certainly are, and cases where they certainly aren't.

(A tangential point: when thinking about software engineering, there are vital issues that have little to do with the choice of the language. For example, a language would have to be truly magical for me to forsake the JVM's vast ecosystem, runtime linking, and runtime profiling and monitoring capabilities – that's why I won't use GHC in my projects, but may certainly give Frege a try)

Dabbling really is not enough to show you the problems with your assumptions. You are right that there is a cost, but it is really paid once up-front by each programmer.

I could have written this same comment five months ago, before I started down the long dark tunnel of doom which is to go beyond LYAH and trying to write a real application. I think it was two months before I saw a light at the end of that tunnel - and only recently that I have really begun to feel as productive in Haskell as I previously was in Ruby. At this point, the costs you speak of are paid up and I do not incur them when I write new code. Yes the type system does impose a lot of structure and some boilerplate, but the benefits are profound. Its the sort of statement I did not really fully credit before experiencing myself, but I will say that is profoundly amazing how frequently my code works perfectly once it compiles. This is particularly true when refactoring. I remember sometimes in Ruby despairing to refactor some code because I didn't want to fix all the tests...in Haskell once my tests are compiling they are passing, and the compiler errors are usually very easy to understand and fix (after several months of head-banging frustration).

I don't doubt you, but your experience also provides little evidence. Have you done profiling and performance analysis yet? Have you tried working in a large team on a large project?

Different languages have different strengths. Some excel at quick and dirty prototyping or small projects/small teams. Others make a 50+ developer team more manageable. Some have good runtime performance, some have a shorter development time, while others have better runtime monitoring and maintenance tools. No language excels in all of these.

Certainly my experience is insufficient. I have only dabbled in profiling and performance analysis - enough to prove it can be done but that it can be time-consuming and that sometimes you may have to give up some elegance/purity in favor of performance. However the performance I get from naive code is so good (so far) that I have not yet actually had to delve into it.

I am dubious of Haskell's prospects in large teams but I was completely dubious of Haskell in general (like you) before I had experience in it. Perhaps after experience with large teams I would be more bullish on this point but I haven't had that experience so far. I am quite optimistic about the size of the problem space that a small, skilled team (say 5 developers) could solve with Haskell.

> Different languages have different strengths.

On the other hand, some things are better in all important respects than something else. Merely stating that among similar things "different ones have different strengths" is not much of a counterargument.

(This comment is not related to any language or technology in particular.)

I also started to study Haskell recently. In my view, it's great from mathematical perspective, but it has its problems.

Monads sort of force you to make plumbing visible, and it's not so neat as a result. For example, consider a big program that has two modules. The module A calls module B to do something. Now later, you want to add logging to the application. In normal languages, you can just call logging functions (which do IO), from within module B; module A doesn't have to know a thing. In Haskell though, you have to wire the IO monad (or some other monad that does the logging and encompasses it) all the way from A to B. Or say, you want now to access database from module B. Again, you have to wire your DB access up from A, because that's where the entry point is.

In normal languages, plumbing like log access, configuration, DB access can be accessed from any place via global variables or singletons, without imposing a dependency on the main module (or other modules). In Haskell, it's like a military installation - every interaction with outside world has to go through main gate. I am not really sure if there is any benefit to it, but there is certainly a downside that the plumbing is becoming visible in Haskell.

If there would be way to declare something as "plumbing" and have it always available (but still explicitly declared as part of function signature), without having to pass it along everywhere, it would be great compromise, I think. It could even make the programs more type safe, because instead of passing RWS or IO monad everywhere, you could make functions dependent on just DB monad for database access, for instance.

Or you could then configure the plumbing for specific modules, something like dependency injection.

Maybe I am missing something, but I tried to find some articles about how to write large scale programs in Haskell, but no one really seems to explain this.

I don't think the "Xy monad will taint all your code" stands.

I used to think that too, but if you have monadic code M and pure code P, if you need to tie P to M (say at a third callsite C), you just lift the P into the monad at C, and that's it. P stays pure, C of course gets monadic, but that is since it _is_ monadic.

Now logging: I guess people overpanic this. There are two separate sides of logging I think:

1) Effect logging: If you want to log effects (going to send the email, etc), you are already in IO, no worries.

2) App logic logging: This is more like debug-logging to verify that you logic works and flows as expected. If you need this in pure code, throw in a Writer monad for logging the stuff (can discard it if not needed).

2a) Eventually you'll get somewhere where you have IO, so you can dump the aggregated logs if you want.

2b) Or just use unsafePerformIO to send to a logging thread (beat me with a stick).

As a bonus, for app logging you might use a proper ADT for your log statements instead of string, which is great for testing, and even greater for persisting (in json, protobuf, whatever) and later inspection.

I am aware of lifting, but the question is if you have monadic code and pure code in different modules (or you need to go through function which was previously pure), where do you put the lift? If you put it outside the module, you break the modularity. If you put it inside, well then you might as well make the functions monadic in the first place. Basically if you have functions in module API (which may be in itself pure) that may eventually end up calling unpure functions, you have to provision for that somehow, either in the module by making them monadic, or in the caller via lifting. Either way, it's not as clean as it could be.

But I thought about it some more, and to me it seems that actually parametrizing the functions to outside world is not that bad; it's a kind of dependency injection, and seems fine. What is really problematic is returning all the IOs (or other monads) from them; especially since you cannot curry return parameters just like you can entry parameters. So even if that could be replaced by some other mechanism, it would be helpful.

But I didn't know unsafePerformIO, sounds like it can be helpful in some cases.

I agree there isn't a lot written that explains this but I have inferred and used the following pattern. I build a massive monad tower that includes the different things I need like a reader with configuration data, a resourcet process monad, a logging writer etc. However I rarely use that monad in my signatures, I generally use a more restricted type class. For example I have a ConfigReader type class that does exactly and only what you would expect. I have a typeclass for writing to the database, for call distributed process, etc. Now because some of those require MonadIO I do end up with a lot of code that could theoretically do any IO. That could be restricted at the cost of creating more and richer typelcass interfaces myself but I do not think it is necessary for the most part.
> but I think that the problem with the way it manages side effects is that it does so through lazy evaluation.

This is not so.

> and lazy evaluation is hard to wrap your head around.

technically haskell is non-strict, not lazy. (a + (b * c)) evaluates + then * instead of * then +. Also strictness annotations can change this behavior.

> They require programmers to think and program solely within Haskell's lambda calculus and lazy evaluation framework, which is neither the way people think nor the way computers do

This is not supportable. You might be more familiar with strict evaluation, but don't pretend its a feature of humanity. All runtimes come with assumptions.

This is another problem with Haskell: comments like this. But in order to be helpful, let me explain why.

> technically Haskell is non-strict, not lazy.

Now, see, I don't care. Neither does anyone really other than PL researchers. I mean, I can care in my spare time if I like spending it on PL research, but when I write a 2 MLOC software for a large customer, I couldn't care less whether "technically" it's "non-strict" or "lazy". As far as I, the programmer, is concerned, it's lazy. If one must be this familiar with PL jargon in order to program Haskell, then this is a problem.

> but don't pretend its a feature of humanity. All runtimes come with assumptions.

Again, I'm not trying to make a provable statement (how does that joke go? you can tell if someone is a mathematician if everything they tell you is all true and all irrelevant). We're talking software engineering, right? So what percentage of production code anywhere in the world is written in an eager (strict, whatever) language? If you tell me it's less than 99.999%, then you're being dishonest. 99.999% is a "feature of humanity". If your point is that education can change people's habit and way of thinking, I say, you're absolutely right. Go for it, and we'll talk again in 15 years.

> All runtimes come with assumptions.

Again, true but irrelevant. Some assumptions are more familiar and therefore feel more "natural",and some are less so.

I wasn't dissing Haskell. It's a very impressive and elegant language. I was only pointing out that while it has some advantages from a software engineering perspective, it also has some disadvantage.

Laziness is an implementation detail that permits equational reasoning and a declarative programming model; the only reasons it's even useful to be aware of haskell's evaluation strategy are 1) to know that it isn't strict (if you're already a programmer), and 2) to solve and anticipate space leaks in production code (if you're using it for Real Work).

Anyway, you seem pretty [sure](http://www.idlewords.com/2005/04/dabblers_and_blowhards.htm) of your opinion so it's probably not worth further discussion.

I don't think I've expressed an opinion because I don't think I have one (I have strong opinions on Scala, but they don't apply to Haskell). I'm just pointing out what seem to be major obstacles to Haskell adoption in the industry. I'm not saying it's not worth the effort because I really don't know. I'm just saying it's not obviously worth the effort. Clearly, the data isn't there yet because there is very little use of Haskell in the industry. This may be unfortunate – or not – but we just don't have enough information to tell yet.

Haskell's roots in academia often steer the discussion towards theoretical PL, which seriously hurts Haskell adoption. I actually like tikhonj's comment because it focused on practicality rather than jargon, so in response I merely pointed out that Haskell is not 100% pure gain in practical terms. That does not mean we shouldn't all adopt it, it just means that the jury is still out.

The laziness-vs-non-strictness part pretty much nailed one problem beginners may have with the Haskell community.

On the other hand, I don't think your 99.something are a feature of humanity, they are the result of the last decades of mainstream programming development. It took me less than a year of on-and-off Haskell hobby fiddling to find functional and non-strict less awkward to think in than imperative and strict.

I think his point is that from an adoption perspective, whether or not it's actually a genetic feature of humanity or merely might as well be is basically irrelevant. Either way, it's a giant hurdle from a practical perspective, especially a commercial one.
> So what percentage of production code anywhere in the world is written in an eager (strict, whatever) language?

How is this relevant?

> If you tell me it's less than 99.999%, then you're being dishonest. 99.999% is a "feature of humanity".

Make all && and || strict in all C, C++, Java, C# code, and chaos will reign.

&& and || are evaluated by first looking at the left operand, then at the right. That seems pretty strict to me. You're thinking of their short-circuit nature, that the right operand is not always evaluated; I believe that that is a different issue than strict vs. lazy.
Consider '||' as a normal function. Consider ||(f(x),g(x)). If this were strictly evaluated, we would compute f(x) and g(x), then pass them as arguments to ||. Instead, we compute f(x) pass it to ||, and compute g(x) only if it is needed. This is lazy evaluation.
> technically haskell is non-strict, not lazy.

What do you mean by that?

> (a + (b * c)) evaluates + then * instead of * then +.

AIUI,

    head 2 [1, 2, (digit_of_pi 1000000000) ]
is forbidden from calculating the billionth digit of pi. That's a pretty strict (play on words intended) kind of laziness, even if not the theoretically maximal definition.
> but I think that the problem with the way it manages side effects is that it does so through lazy evaluation, and lazy evaluation is hard to wrap your head around.

Haskell manages side effects by making assertions about whether a function has side effects part of its type. Lazy evaluation isn't quite orthogonal to that, but the connection goes in the other direction from what you imply. Because evaluation order in a non-strict language can be complex to reason about, such languages become impractical if unrestricted side effects are allowed. In other words, restricted side effects helps non-strictness, but non-strictness is not necessary to restrict side effects.

Though as SPJ described in "Wearing The Hair Shirt", non-strictness might be necessary to motivate language designers to sufficiently restrict side effects...
> ... with the way it manages side effects is that it does so through lazy evaluation

Modern Haskell manages effects through it's type system, not through lazy evaluation.

> They require programmers to think and program solely within Haskell's lambda calculus and lazy evaluation framework, which is neither the way people think nor the way computers do

Haskell doesn't require you to think in the lambda calculus any more than C requires you to think in register allocations, it's a low level implementation detail that effectively gets abstracted away. Nor are you required to think solely in terms of pure code, Haskell 2010 has a whole variety of ST Monad solutions where you can effectively do whatever pointer manipulations you want and still remain pure with respect to the rest of your program. I take serious issue with the description of Haskell being "constrained", it has reached a compromise of safety and power that I have yet to see in any other language.

> the way it manages side effects is that it does so through lazy evaluation

This is actually not true. Lazy evaluation forced Haskell to stay pure, but it is purity that provides the tools for constraining effects. You could have a strict Haskell-like language that manages effects in the same way, indeed such languages exist.

> You could have a strict Haskell-like language that manages effects in the same way, indeed such languages exist.

I'm unaware of any strict purely functional language with monads. Could you give an example? Thanks!

Idris is one example.
Disciple[1], and I believe "Mu" Haskell ( the Standard Chartered dialect ) though I don't know for sure because it's not open source.

[1]: http://disciple.ouroborus.net/

Isn't the side effect returned as an IO type which is later evaluated by the runtime? (I'm talking about Haskell)
Yes, but you can create the thing of type `IO a` strictly.
Yes, but when are they executed? When is the byte written to the file?
Well, you certainly can't start executing until the `IO a` thing has been evaluated, but there aren't any other constraints.

Something that can confuse people (and I'm not sure if this is the case with you or not) is that an `IO a` may/probably will contain a closure. So evaluation and execution are interleaved, but not because of laziness.

You can picture functions with the IO type as something to compose a program out of. This program is then run by runtime system of your implementation, and that's "when the byte is written".

Here's a nice explanation of Haskell IO: http://stackoverflow.com/questions/13536761/what-other-ways-...

> They require programmers to think and program solely within Haskell's lambda calculus and lazy evaluation framework, which is neither the way people think nor the way computers do (the latter is important when doing performance analysis on your code). This constrained framework also takes its toll on software engineering, then.

I disagree. People don't automatically think in certain terms unless they're taught to.

I personally never took CS in University, and I'm completely self taught as far as programming goes. Haskell feels quite natural, so does Lisp. Java and C feel foreign.

Maybe if someone's had imperative languages drilled into their head for 4+ years Haskell will feel foreign, and maybe that's why mathematicians, financial people and others who don't quite fit the typical programmer paradigm are into Haskell (or so I hear - I always hear about Haskell and OCaml being for 'academic' types).

To me, Haskell has nice syntax, makes sense, is fast, and feels like a dynamic language for the most part with its type inference and great interpreter...

Sounds like c# and the .NET platform might fit that criteria in yoyr tangential point.
Thanks tikhonj for the very well articulated response. I wish that this thread had been pointing directly to your comment rather than the blog post that you responded to!
It looks like your comment has been "moderated" off the blog. Such a disappointment.
With regard to declaring the lack of side effects in an interface, I'd just like to mention that C++ is very nice in this regard too, and I think it's an important feature of C++ that is often overlooked.

Yes, it's true that with casting and so on you are not actually ensuring anything when you declare a function const like you are with Haskell, but you announce to other programmers who will use your code that:

1) There will be no observable state changes

2) That the function is thread safe

This is a very useful thing for a language to support in it's function declarations, and other languages could do well to learn from that.

That is not true. In C++ you can always retrieve the current time, store parameters in a database, print to stdout, or retrieve a global variable without changing the interface. Merely announcing purity to other programmers does not solve this problem: announcements can be wrong, missing, incomplete, and maybe most importantly the compiler doesn't know about them.
Heck, mere "announcements" might even be quite useful, especially since it is slightly compiler supported. But const in C++ does not announce purity, nor does it announce thread-safety. I just announced that it won't observably change non-mutable members, that's it.
D actually has a better model for pure functions than C++. See http://dlang.org/function.html#pure-functions
Modern C/C++ compilers can detect a sub set of pure functions and do possible aggressive optimisations. There's even an `__attribute__ ((pure))` in GCC for explicit pureness.
I think the point of the argument about functional programming is that you can't defend a stance of absolute rejection of all things non-functional, because that kind of world could not exist. Similarly, an all-object world does not make sense, either.

His other point about OOP, though, that it doesn't have first-class functions -- that's equivalent to complaining that it isn't functional enough, which is not supposed to be the point of OOP, anyway. There's a lot you could criticize about OOP, but he doesn't make that strong of a case here. He's sort of treating it as a mirror image of fp, but it came from a different world with different values and it's not on the same footing.