
Do notation considered harmful - jim-jim-jim
https://wiki.haskell.org/Do_notation_considered_harmful
======
andolanra
This is a very old opinion, and one I don't think has aged terribly well. How
old? Partway down the page there's a link called "new developments" that cites
GHC 6.12, a version that came out in 2009!

Some thing it cites are still reasonably true, but others are complaints that
reflect the Haskell of a different era. For example, it says, "Even more
unfortunate, the applicative functors were introduced to Haskell's standard
libraries only after monads and arrows, thus many types are instances of Monad
and Arrow classes, but not as many are instances of Applicative." This has not
been true for about half a decade, when Applicative was made a superclass of
Monad. The article follows with: "There is no special syntax for applicative
functors because it is hardly necessary." This is also no longer true, since
the introduction of ApplicativeDo: even if you don't like it (and I confess
I'm not a huge fan of ApplicativeDo myself!) it turns out that many people do
find that sugar easier to write.

However, I think the key point I'd make is: all the advantages listed here of
using monad without do-notation are real in some cases, but there's nothing
stopping you from _not_ using do-notation sporadically! Occasionally a snippet
that uses do-notation can be made much clearer by using >>= or some other
monadic function, and occasionally a snippet that uses >>= can be made much
clearer by using do-notation, and I don't think there's anything inherently
wrong with Haskell keeping both. To make a Python analogy: sometimes you can
express a given construct more clearly with a loop than with a list
comprehension, but I don't think that list comprehensions should be considered
harmful: just use those tools when you need them, and don't use them when you
don't need them!

~~~
jcelerier
> [https://gitlab.haskell.org/ghc/ghc/-/wikis/applicative-
> do](https://gitlab.haskell.org/ghc/ghc/-/wikis/applicative-do)

can't help but chuckle at the accomplishment of 40 years of research in
functional PL, ending up with almost C-like syntax once again.

~~~
rednum
I think there's other way of looking into this: in Haskell we have well
defined semantics (when you look at function you have explicit information
about what is happening) and we managed to get to familiar syntax. In C we
have very messy semantics: "everything can happen everywhere". Java stays
somewhere in between (though closer to C side): when you have a method in some
class it most likely touches only visible methods/fields of its arguments and
fields of this class (obviously there are some escape hatches like
unsafePerformIO, but most of your codebase will not use this).

I believe being able to understand code on high level quickly is a good thing
in engineering. Wouldn't it be nice if looking at some part of code you could
say that it actually is calling microservices X, Y and Z and no other
microsevices? In Haskell you could have that. I don't see any fundamental
reason why such functionality couldn't be brought into an imperative language
at some point (but maybe I'm ignorant about PL theory). (FWIW I don't believe
in Haskell going mainstream at any point).

~~~
HelloNurse
The do notation allows the reader to _misunderstand_ code quite deeply,
contorting sound, standard and relatively simple functional programming
techniques and idioms into an obfuscated and deceptive syntax that looks like
something it is not.

~~~
tromp
It's just syntactic sugar for Monad functions >>= and >> How can that be
deeply misunderstood?

~~~
whlr
Because it looks imperative.

~~~
pinopinopino
Do notation is jokingly called a programmable semicolon. True, it is often
used to model imperative computations with it, so that is the association
people have with it. Also people come mostly from imperative backgrounds and
look, we can bind something to a value. Finally something I get! And that is
fine, let them play around with it like that.

But if they look closer, they find out it is not really imperative. Why is
there a difference between let and bind? Why can't I reassign values. And they
start to use more exotic and also more interesting monads and boom the
imperative feel goes away.

For example with the interesting LogicT[1] monad. Or what about the
probability monad[2]? Or the continuation monad[3]? Or the reverse state
monad[4]? How imperative are those really? It really depends how you use it.
And I think it adds more to the magic, that you think it is just imperative
code at first, because suddenly you can create code that behave very different
under various monads that represent computation models. I think that is a cool
thing about this confusion.

I understand that if you are a professional that needs to get work done, it
can be annoying.

[1][https://hackage.haskell.org/package/logict](https://hackage.haskell.org/package/logict)
[2][https://hackage.haskell.org/package/probability](https://hackage.haskell.org/package/probability)
[3][https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-M...](https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-
Monad-Cont.html) [4][https://lukepalmer.wordpress.com/2008/08/10/mindfuck-the-
rev...](https://lukepalmer.wordpress.com/2008/08/10/mindfuck-the-reverse-
state-monad/)

------
alephu5
In terms of pedagogy I think there's a valid argument that we shouldn't teach
beginners about the do notation until they're comfortable with monads. Coming
from an OO background they can grok typeclasses as interfaces quite easily,
and the bind and return operations as special methods. It's probably better to
use pure instead of return but that's another debate.

In the same way I never tell python beginners about list comprehensions, we
just do pedestrian loops.

In both cases, as you get comfortable and learn more you'll naturally stumble
across these features and a myriad of others. Once you can happily read and
write both variants, it doesn't matter too much, it's really just a matter of
taste. I sometimes combine both in a single expression doing stuff like this

do x <\- a >>= b ...

for x in (f(t) for t in ts if g(t)): h(x)

~~~
tome
> do x <\- a >>= b ...

Please at least keep everything flowing in the same direction!

do x <\- b =<< a ...

------
twblalock
I like that Haskell exists, but stuff like this article demonstrates why it
isn't adopted widely.

Imperative programming is the most natural thing for people. "Do this, then
that, then that, in that order."

Some languages take that model and run with it, and add features that are not
"pure" but let people get work done. We all know which ones those are. If you
work in industry they are the ones you use.

Haskell rejects the imperative model, yet it attempts to appear imperative to
be palatable to more people while retaining a pure functional model under the
hood. But that causes it to be criticized by the pure-minded. So who is it
for?

~~~
otabdeveloper4
> Imperative programming is the most natural thing for people. "Do this, then
> that, then that, in that order."

No way. Have you ever mentored technical people who aren't specifically
trained programmers by education?

These kinds of people will have no problem writing huge SQL queries with joins
nested four levels deep, or pages and pages of declarative Pandas or R code
using weird transformation and filter logic, but they'll have humongous
problems understanding simple loops or variable assignment.

Another data point to consider: Excel is the most popular programming
environment in the world, and it's also declarative, not imperative.

~~~
cousin_it
Yes, Excel and SQL are widely used by non-programmers due to some engineering
quality. But whatever that quality is, Haskell (very narrowly used even by
programmers) doesn't seem to share it.

~~~
otabdeveloper4
I'm trying to say that whatever makes Haskell unpopular, it's not its
declarative nature.

Whoever manages to make a purely declarative general-purpose programming
language will be a technological superstar, because for normal non-programmer
folks it would be the ideal gateway to programming.

(That said, I don't think it's possible. 'General purpose' implies some sort
of Turing completeness, i.e., recursion or loops, which immediately makes it
impossible to reason about in normal declarative terms.)

~~~
wwright
I think we can be a little more optimistic than that. I agree that it’s
probably a white whale to try to build a language which is fully declarative
100% of the time, but if we can make something that is simple and declarative
80% of the time, but falls back to something as graceful as Python for the
other 20%, that may be enough to have a really big impact.

------
tom_mellior
> There is no special syntax for applicative functors because it is hardly
> necessary. You just write

> readHeader = liftA3 Header get get get

Seeing Haskell code peppered with liftM_ and similar things is one of the main
problems I have with the language. Monads (and applicatives etc.) solve a
problem, one might even say that they solve it well, but I've never found that
they solve it _ergonomically_.

~~~
kqr
Yes, I think you have a key point there. Using the same-ish syntax for effects
as for pure code is a tricky problem of ergonomics: get it too
similar/implicit, and people will be confused about when effects happen and
when pure code runs. Get it too dissimilar/explicit, and it gets cumbersome to
write.

I actually think do notation hits a great middle point, where it's obvious
you're in effectful code, yet familiar and similar enough to pure expressions
to be convenient.

------
mrkeen
Do-notation papers over map and bind in the same way that for-loops paper over
cmp and jmp.

I do not want to go back to map and bind.

------
sfvisser
Not a very compelling article.

None of the drawbacks stated are really serious issues, or issues at all.
Everyone with a bit of experience will be aware of the possible drawbacks and
knows how to avoid this.

Newcomers probably have other things on their mind. Their app is not going to
break, because their code is somewhat clumsy.

And more importantly: do-notation is really awesome!

------
unhammer
> These misunderstandings let people write clumsy code like do putStrLn "text"

Is it really a problem that newcomers write things like that? It doesn't make
the program any slower or more buggy. It's trivial for a code-reviewer to fix.
If the new haskellers have hlint installed, they'll get a hint to simplify it
and eventually they'll learn the idiomatic way. (And the readFile/writeFile
example was just fine before being "fixed".)

~~~
_ikke_
For me, that's similar to writing

    
    
      if(var == true) {
    
      }
    

It's more verbose than necessary, but not wrong.

~~~
iso-8859-1
In Python and JavaScript it is also asserting the type of the thing being
compared, which, I argue is better code style. It is a mess having to think
about all those things being truish.

------
bad_user
Article says that this:

    
    
        readFile "foo" >>= writeFile "bar"
    

is preferable to this:

    
    
        do
          text <- readFile "foo"
          writeFile "bar" text
    

I don't agree. Even if a working knowledge of the monadic bind is required,
the second expression is more readable to beginners and can be extended more
easily to do other things.

Of course, the first expression is written in "pointfree" style, where the
code is described as a composition of functions. Even if I like pointfree one
liners that are clear (so depending on context), I hate pointfree style
applied to entire codebases and I think it's one barrier for Haskell's
adoption.

\---

Article also says: " _Newcomers might think that the order of statements
determines the order of execution._ "

But that's good, because the order of statements in a "do" expression _does
determine_ the order of execution. That's the whole point with monads and the
"do" expression. That you can get away with applicatives instead, that's
another point entirely.

Note that GHC nowadays has a language extension called "ApplicativeDo". So to
take an example from the article:

    
    
        do x <- Just (3+5)
           y <- Just (5*7)
           return (x-y)
    

In this case you just enable " _ApplicativeDo_ " and it will magically use
applicatives without changing the code.

The order of execution on the other hand will NOT matter, so you can still
pretend that there's an order to it, because such code will be pure and it
will not be repurposed to run in parallel or anything crazy like that, the
only thing the compiler can do in such cases is to eliminate unused
expressions.

Funny thing about Applicative being a super class of Monad, but "ap" / "map2"
operations need to remain consistent with "bind" by law otherwise the
implementation isn't valid. So it doesn't matter if the code above uses
applicatives or monads, that's just an optimization, as for all practical
purposes the result should be the same, order of execution included.

Of course, code using Applicative instead of Monad is more generic, since you
have less restrictions, you can work with more types and the function
signatures become more clear in what they do (the more abstract they are, the
less they can do), so that's the primary reason for why Applicative is
recommended whenever you can get away with it. No reason to bring the Monad
chainsaw to the party when not needed.

~~~
tome
> the order of statements in a "do" expression does determine the order of
> execution

Not really, unless you take a very specific definition for "order of
execution". There are plenty of monads where order of statements is unrelated
to the order of execution, Identity, for example!

~~~
bad_user
I don't agree. Bind creates a data dependence that forces an evaluation order.

    
    
        do
           x <- f
           y <- g x
    

No matter what Monad you're speaking of, in this expression "x" is going to be
evaluated before "y", what we sometimes call a happens before relationship,
because evaluating "y" depends on evaluating "x".

~~~
tome
What are you basing your disagreement on? Can you explain to me in which sense
"x" is evaluated before "y" in this example?

    
    
        {-# LANGUAGE BangPatterns #-}
        
        import Data.Functor.Identity
        import Debug.Trace
        
        f = let !_ = trace "Evaluating f" ()
            in return 1
        
        g x = let !_ = trace "Evaluating g" ()
              in return (x + 1)
        
        example :: Int
        example = runIdentity $ do
          x <- f
          y <- g x
        
          return y
    
    
        > example
        Evaluating g
        Evaluating f
        2

------
dasKrokodil
The do-notation is one of the best features of Haskell.

------
rtpg
I feel like there’s so much self owning by Haskell and Lisps in making
multiline statements such a chore.

Why does introducing let add indentation? I know why but it’s not useful! I’m
just trying to provide a name for an expression, and my pseudo code wouldn’t
have an indentation

Do notation is an out, but it’s its own special thing. So you get people
writing nomadic interfaces because it’s the most syntactically convenient.

~~~
tome
Then use {} around your binders. This sets x to 11, for example.

    
    
        x =
        
            let { y
                             =
        
           5
         
                ;
         
          z =
        
        
            6 }
        
                           in y
    
          + z

------
themagicalcake
isn't IO inherently not functional? it's a side effect

~~~
saithound
To add to lgas' point:

The distinction between functions with side effects and descriptions of what
IO will be done at run-time has many practical consequences. For example, if
you execute the Haskell code

    
    
      greet name = do
        putStrLn ("Hello, " ++ name)
      repeat3 action = do
        putStrLn "Doing it thrice."
        action
        action
        action
        putStrLn "Done it thrice."
      main = 
        repeat3 (greet "themagicalcake")
    

you get the following output:

    
    
      Doing it thrice.
      Hello, themagicalcake
      Hello, themagicalcake
      Hello, themagicalcake
      Done it thrice.
    

But if you execute the similar-looking Python script

    
    
      def greet(name):
        print ('Hello %s' % name)
      def repeat3(action):
        print ('Doing it thrice.')
        action()
        action()
        action()
        print ('Done it thrice.')
      if __name__ == "__main__":
        repeat3(greet('themagicalcake'))
    

you get

    
    
      Hello themagicalcake
      Doing it thrice.
      Traceback (most recent call last):
        File "test.py", line 10, in <module>
          repeat3(greet('themagicalcake'))
        File "test.py", line 5, in repeat3
          action()
      TypeError: 'NoneType' object is not callable
    

instead. The Haskell value `greet "themagicalcake"` has type `IO ()`: it is a
value that describes IO to be done at runtime. In contrast, the Python value
returned by the call `greet('themagicalcake')` has type `NoneType`, and this
value does not describe anything. Calling `greet` itself had the side-effect
of printing "Hello themagicalcake".

~~~
lalos
would this work on python?

repeat3(lambda: greet('themagicalcake'))

~~~
saithound
It would work in the sense that it would not throw an exception, but it
wouldn't work the same way in every situation. Haskell's `greet
"themagicalcake"` does not correspond to Python's `greet('themagicalcake')`,
nor does it correspond to Python's `lambda: greet('themagicalcake')` in every
situation. E.g. you can write

    
    
      main = greet "themagicalcake"
    

in Haskell to get a program that just prints

    
    
      Hello themagicalcake
    

but the Python program

    
    
      if __name__ == "__main__":
        lambda: repeat3(greet('themagicalcake'))
    

prints nothing.

------
infinity0
The article is obsolete and should be deleted, there is ApplicativeDo these
days.

------
ncmncm
"Get" functions, too.

And type names with "state" or "metadata" in.

