3 ms·
I just skimmed but IIUC, the article is a little more of an in-joke than an explanation and does seem to expect its audience to already understand (or maybe be
by dllthomas 6y ago
I just skimmed but IIUC, the article is a little more of an in-joke than an explanation and does seem to expect its audience to already understand (or maybe be intrigued enough to learn more from other sources?).
That said, I can try to explain here:
Haskell has a bit of syntax available called "do notation". You can write Haskell without it, but it makes some things read better (as a matter of common but not universal opinion).
There's a simple, purely syntactic translation from do notation into regular application of functions. "Syntactic sugar causes cancer of the semicolon." There are four rules, none of which is complicated, and only two of which are relevant here:
First, a single expression is just that expression, nothing magic happens.
do
expr
simply becomes
expr
Next, the arrows:
do
x <- m
... more stuff, which might use x ...
becomes
bind m (\ x -> do
... more stuff, which might use x ...
)
or in a few other syntaxes:
bind(m, x => do ... more stuff, which might use x ...)
(bind m (lambda (x) do ... more stuff, which might use x ...)
m.bind(|x| { do ... more stuff, which might use x ...)
bind(m, [] (auto x) { do ... more stuff, which might use x ...)
That internal do is then expanded recursively.
So to translate the whole block that's repeated throughout the article:
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
becomes
bind getData (\ a -> do
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d)
which becomes
bind getData (\ a ->
bind (getMoreData a) (\ b -> do
c <- getMoreData b
d <- getEvenMoreData a c
print d))
which then becomes:
bind getData (\ a ->
bind (getMoreData a) (\ b ->
bind (getMoreData b) (\ c -> do
d <- getEvenMoreData a c
print d)))
and then:
bind getData (\ a ->
bind (getMoreData a) (\ b ->
bind (getMoreData b) (\ c ->
bind (getEvenMoreData a c) -> (\ d -> do
print d))))
and finally
bind getData (\ a ->
bind (getMoreData a) (\ b ->
bind (getMoreData b) (\ c ->
bind (getEvenMoreData a c) -> (\ d ->
print d))))
Which is "just" a bunch of chained functions combining lambdas.
So... why bother? and how does it do so many different things? and how does it know which to do? and what does it even do?
The key is that we're overloading "bind", picking the behavior that we want. You could do this in most languages by passing in a choice of function. We could have "do" take a parameter, like
do(bind)
or in an OO language you might hang bind on the objects involved. We often see this for particular instances - for instance, .then for promises/futures.
Haskell does it with a mechanism called "type classes", where you can specify an implementation of an interface for a given type, and the compiler will figure out which implementation to provide. This is very similar to Traits in rust, implicits in Scala, etc. You can usually avoid specifying the types manually because of inference.
So in Haskell, Monad is an interface providing two functions:
class Monad m where
bind :: m a -> (a -> m b) -> m b
pure :: a -> m a
We've already come across `bind`, which lets us operate "inside" a context in a way that combines the contexts.
The other function included, `pure`, takes a value and gives it the minimal possible context. What that means is implied by the "monad laws", which tell us how `bind` and `pure` must interact.
For each type that "is a monad", we tell the Haskell compiler how that type implements the interface:
// optionality:
instance Monad Maybe where
bind Nothing _ = Nothing
bind (Just x) f = f x
pure x = Just x
// state:
newtype State s a = State (s -> (a, s))
instance Monad State where
// pure gives the State action that produces x and leaves the state unchanged
pure x = State (\ s -> (x, s))
// bind threads the state through the chained actions
bind (State f0) f1 = State (\ s ->
let (x, s') = f0 s
State f2 = f1 x
in f2 s')
// lists:
instance Monad [] where
pure x = [x]
// bind for lists is concatMap
bind [] _ = []
bind (x:xs) f = f x ++ bind xs f
... that got long. Please ask any questions you're left with :)