Hacker News new | ask | show | jobs
by ufo 688 days ago
One thing that many people miss is that Haskell's monadic style is a direct consequence of lazy evaluation. It all started because they thought lazyness was nice, and wanted to make a language that brought that front and center. But then they found out that they had to come up with a new way to do side-effects, because traditional side-effects don't work when the order of evaluation is unpredictable.
2 comments

> One thing that many people miss is that Haskell's monadic style is a direct consequence of lazy evaluation. It all started because they thought lazyness was nice, and wanted to make a language that brought that front and center. But then they found out that they had to come up with a new way to do side-effects, because traditional side-effects don't work when the order of evaluation is unpredictable.

But this is not true, is it? I thought that Haskell solution for side effects was just tagging the functions with side effects, and make the compiler handle functions with side effects as regular programs, no laziness, no memoization, no reordering. 'Monad' is just a mathematical structure that regular programs obey, also 'do notation' is just a style that allows Haskell programmers to write imperative style code.

Monads don't have anything to do with laziness but historically the need for them arose because of laziness. It's the first thing explained in the introduction of Tackling the Awkward Squad:

"Call-by-need (or lazy) languages, such as Haskell, wear a hair shirt because their evaluation order is deliberately unspecified. Suppose that we were to extend Haskell by adding side-effecting “functions” such as printChar. Now consider this list

  xs = [printChar 'a', printChar 'b']
(The square brackets and commas denote a list in Haskell.) What on earth might this mean? In SML, evaluating this binding would print 'a' followed by 'b'. But in Haskell, the calls to printChar will only be executed if the elements of the list are evaluated. For example, if the only use of xs is in the call (length xs), then nothing at all will be printed, because length does not touch the elements of the list.

The bottom line is that laziness and side effects are, from a practical point of view, incompatible. If you want to use a lazy language, it pretty much has to be a purely functional language; if you want to use side effects, you had better use a strict language.

For a long time this situation was rather embarrassing for the lazy community: even the input/output story for purely-functional languages was weak and unconvincing, let alone error recovery, concurrency, etc. Over the last few years, a surprising solution has emerged: the monad. I say “surprising” because anything with as exotic a name as “monad” — derived from category theory, one of the most abstract branches of mathematics — is unlikely to be very useful to red-blooded programmers. But one of the joys of functional programming is the way in which apparently-exotic theory can have a direct and practical application, and the monadic story is a good example. Using monads we have found how to structure programs that perform input/output so that we can, in effect, do imperative programming where that is what we want, and only where we want. Indeed, the IO monad is the unifying theme of these notes."

https://www.microsoft.com/en-us/research/wp-content/uploads/...

Haskell showed that a monadic bind is a nice solution to chaining together IO operations once you have boxed them inside their own type and boxing IO operations inside their own type was a nice solution to the issue arising from being lazy by default.

Haskell was actually widely successful in showing that, no doubt about that.

The oldest mention of Haskell Monads that I ever had in my hands starts the description with "this nice hack from the Haskell group at University of Glasgow to make I/O nicer without bragging laziness"
I wouldn't say that's an accurate description of how Haskell handles side effects. In fact I wouldn't say that Haskell has side effects at all.
Well, it depends on exactly what you mean by side-effects.

First, obviously you have unsafePerformIO, so that everything can have side-effects.

Second, you have side-effects like using memory or using the CPU. You are not supposed to worry about those. Though a more serious side effect you do have to worry about is non-termination. Haskell doesn't track that in its type system.

You are right that the way input/output is handled can be described not as _side-effects_ but as _effects_ of interpreting values of the IO datatype.

I think this is historically wrong. Monads didn’t land until later in Haskell, no?
They did, but they also did land explicitly to make I/O suck less in lazily evaluated language instead of magic main function signature working to provide explicit ordering.

There's a reason why Monads aren't exactly monadic and why IO was the original monad in GHC -as well as why non-lazy, non-super-pure languages never really go for Monads

Actually, lots of those more pragmatic languages go for monads, they just don't use them for input/output.

The way JavaScript handles async is pretty close to monadic. Error handling via the local equivalent of Maybe / Either is monadic. Tuples are monadic. Sequences can be monadic. Etc. Some languages have a flexible enough type system to expose this (like Haskell), some don't. Some like Rust generally don't expose monads to the type system, but their users are aware enough of the shared monadic structure that you can see it reflected in the naming conventions for functions that do essentially the same thing (in a monadic sense) but for different structures.

I don't think GP is contradicting that.