Hacker News new | ask | show | jobs
by pron 4607 days ago
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)

7 comments

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.
Well, the Boolean operators in C-like languages are NOT normal functions, which is the point. Furthermore, what is strict about them is the order in which their operands are evaluated, which is a different aspect than the one you mention: whether all arguments are evaluated before considering the function. Consider e.g., notUnderAttack() || enableDoomsdayDevice(). A lazy language is free to decide that it's more optimal to evaluate the second operand first.
> 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.

Closures can be a way of implementing laziness, so I'm not sure you've shown it's "not because of laziness" - though certainly it's not because the language is lazy by default.
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.