do
notationThese dialogue snippets are from a series of conversations occurring over months, but we’ve edited them to try to present a mostly coherent presentation of what we think about do
notation and why.
The first time we realized that we have a disagreement came in a conversation about Applicative. This was right before I was going to teach a class about Applicative and was trying to figure out what syntax people unfamiliar with Applicative find easiest to read:
I know every time I unthinkingly put something like
(+) <$> Just 4 <*> Just 5
in front of a student who isn’t ready for it, they freak out.
I just recognize the pattern and mentally translate it into liftA2
.1
Huh, I always forget liftA2
exists. And I think it obscures, although not as bad as do
syntax. So I think the way that will be the most helpful is to present (Just (+ 4)) <*> Just 5
first, then the version using pure
, then the infix-only version.
I think there’s a contrast between learning-Haskell and real-world-Haskell here.
There often is, but I want to know specifically what you mean.
When I’m writing software, I kinda want to “obscure” in that way with do
notation. The lifting and the applying, they are a distraction from the higher-level idea I am trying to communicate. I know that thinking this way is often a trap, a seductive mistake. So I don’t know how to justify why I think it’s right in this case.
You think directly lifting/applying is more of a distraction than do
syntax is? This is curiosity, not argumentation, although I find do
syntax distracting.
Yeah, but I don’t know why.
I find it distracting because there is often more I have to read (or I feel like there is) before I know what’s happening. There’s this extra step of renaming stuff. That is not always the case — sometimes do
syntax is ok, depending on what’s inside the block.
This series of tweets led to another conversation:
Is that the book parser you’re writing all in do
notation?
Yeah.
Why? I don’t get it. You sound like it’s obviously better. It’s frustrating for me when people act like like something is obviously better in some way, but I can’t see why. Please tell me what I’m missing.
Hmm. I don’t think I know why it’s more clear to me. For small expressions I don’t think it makes much difference.
I guess what I like is how it lets you only specify the structure of the thing you’re parsing first, and then how to assemble the values, separately, on the last line, rather than having to think about both of those things at once.
I see. Fair enough, at least you have a reason.
Also it eliminates parens. Paren nesting can get excessive in parser expressions. Or perhaps my style could be improved.
I don’t know. For me do
notation is obviously bad, but I seem to be the only person on Earth who thinks so. Which is OK, I guess. I have my reasons, you have yours. But what’s obvious to me isn’t to anyone else I guess, and vice versa.
I’m snarking a little with the obviously bad. It’s not all bad.
Is this one of those things that is a result of learning Haskell first you think?
That thought had crossed my mind. It’s like… you seem to think more natively in syntax trees than the typical programmer, whereas “linear” thinking comes first to me. It’s a comfort to me when I can think of a computation in sequential steps. A parser that consists of parsing a
, then b
, then c
, then combining the results.
That could be related to linguistics too. But it could be I think like that and so am attracted to generative syntax and Haskell because they make sense to the way I already think. Or it could be that doing a lot of syntax made me think a certain way. Or some reinforcing combination.
I do think more sequentially, or linearly, sometimes. And if that’s how I’m thinking of it I sometimes use do
notation. But then I usually end up trying to refactor it once I — well, I’d say “once I understand what I’m really doing.” For me that probably means understand the underlying shape of what I’m doing, i.e. the tree. Part of why I dislike do
notation is for big things where I want to see some intermediate type information, it’s a bit harder, though ScopedTypeVariables
makes it tractable, I suppose.
And it feels to me like it’s hiding the functors, it’s hiding the mappings. I get this is not how everyone feels, and I asked you because usually you can explain why you said something, and it helps.
Yeah, I think it’s hiding the functors. There’s a sort of “magic” feeling to it at times.
I want my functors out in the open, Chris.
A couple days later, as Julie was editing the Parsers chapter of Haskell book, she noticed there is a ton of do
notation in that chapter. And that led to another conversation:
I just noticed there’s do
notation all over the Parsers chapter. I didn’t have much of an opinion about do
syntax at the time we wrote it; this is a developing crankiness on my part. Take this for example:
parseSection :: Parser Section
parseSection = do
skipWhitespace
skipComments
h <- parseHeader
skipEOL
assignments <- some parseAssignment
return $
Section h (M.fromList assignments)
I think that’s a good example of something that is easier for most people to read in do
notation than it otherwise would be.
Yeah. Because it has a lot of “skips”?
Yeah and not so much binding of results to different names, names that you then have to look through the rest of the block to see where they get used.
A little while later:
“Functions are in my comfort zone; syntax that hides them takes me out of my comfort zone.”
— Paul Hudak on do
notation. I am in good company with my bad opinions!
On the other hand, I like this perspective as well.
Maybe it’d have been culturally better if do
notation had been an extension.
I don’t know. It’s probably correct that it’d have even less wide adoption now without do
syntax. But, uh, avoid success at all costs, right?
I think thinking of it as an advanced feature might be helpful. I agree it obscures what’s going on, and when I use it it’s often in situations where I don’t want to think about what’s going on, not at that low level.
Are you suggesting I think at low levels? devilish grin. More seriously, yeah, I get that.
Making it an extension would make it clear that you don’t need it for anything which might help.
Yes. I’m just doing this thing where I’m surrounded by people who seem to all agree on their love of do
notation, so I feel like my opinion is actually wrong but I just don’t know it because I’m not a real enough programmer. So I’m seeking validation that it’s OK to have my opinion. You already have the confidence to have your opinion, but I do not.
My opinions change a lot, though.
It doesn’t matter, because you have great confidence in them while you hold them! But Paul Hudak is on my team here and he is a very real programmer — or was, before he died.
It seems we end up discussing Chris’s controversial tweets a lot. This conversation started from this series of tweets. It turns out that Chris has a lot of opinions about for
and traverse
and we’re always ready to argue with each other (amicably):
Ha, I forgot that
traverse print [1..10]
prints the list of unit values at the end.
Yeah, it really should have been traverse_
.
Yes, it was a mistake but now I got me a list of units.
I ran it in the REPL, and it looked good enough. I didn’t notice the result value.
How did you miss the big list of units? Heh.
If I had to do it all over again I’d
for_ [1..10] print
I like the foldMap
one better. I forget we have a for_
and it scares me.
Yes, foldMap
is better. Gabriel wins.
I’ve never seen anyone write Haskell the way you do sometimes. It’s both frightening and impressive.
Our shake
config uses for_
a couple times. And most of the main
of haskell-to-tex
is in a for_
.
Figures. You would do that.
Java-born, for
loops in my blood.
I think all the manners of traversals is something we can help clear up in the book.
That’s funny what you said, though, since for
is flip traverse
, isn’t it? That’s why traverse
is clearly easier. To be honest, things like for
irritate me for other, completely unrelated reasons as well.
So how would you rewrite this expression?
for_ (Map.toList (fileSnippets file)) $ \(name, snippet) -> do
putStrLn $
"File " <> name <> " - " <>
tshow (Foldable.length snippet) <> " paragraph(s)"
writeFile (outDir </> Text.unpack name) $ fold
[ "% Generated from "
, Text.pack (takeFileName inFile)
, "\n\n"
, "% Generated by haskell-to-tex-"
, Text.pack (showVersion version)
, "\n\n"
, renderSnippet snippet
, "\n"
]
I’m not saying I would. I’m sure there are times when it makes sense. But if you don’t know for_
exists, then you’d find a way, right? So, how would you write it if you didn’t know for_
existed?
In that case I’d introduce a named function and reverse the order of the arguments.
Sure, that’s what I would have done. I do that a lot anyway because it’s easier for me. It is probably a failure of my brain.
It’s usually easier.
I do not want to manipulate big things in my head, all at once. I need little things, little parts. It’s similar to what you said about why you like do
for your parsers: you can think one step at a time instead of having to consider the whole progression.
for_
might be a special case, for people like me used to it being a special construct in other languages.
Yeah, that’s part of my worry about it.
Yeah, that makes sense.
If all my Java and Scala dev students find out about it, they will assume it’s the same thing they’re used to and not learn about traverse
.
I wonder if that’s part of the root of our difference about do
notation, too. They’re things that make it easier to write large expressions.
For sure it is.
When the goal should be: don’t do that.
Pedagogically, I want people to be able to see how the types work out. I want them to get used to thinking in types before they start relying on things like that hide that information from them. This may not apply to for_
but it’s not great with huge do
blocks. You just end up with a big incomprehensible chunk. Like in Java. I’ve seen people do that.
Big do
blocks also lead to situations where you just have no idea what any of the types are
so type annotations help — at which point, you might as well also give them a name and break them off.
Yes, exactly. I think most of the conflict we have like this is coming essentially from two things: 1. Our backgrounds (you from Javaland, me just not knowing anything about programming except a little bit about Haskell), and 2. I’m always thinking about the pedagogical ramifications rather than what I want when I write code. Or, heaven forbid, software.
Yes, I’m rewriting the haskell-to-tex
thing in smaller pieces without do
or for_
and it looks nice.
It wasn’t a criticism of your code. You don’t have to do this.
The bigger improvement in that code was I had a bunch of stuff that could be moved out of IO
functions.
I’ve been thinking about starting a FITEME series of blog posts, including one about do
notation, so i may quote you:
“Don’t focus on connecting Monad and Applicative to do
notation; that’s true, but it’s a distraction.”2
Hue hue.
Bonus:
I am distracted today. I’ve made fantastic progress at learning some things I really wanted to know. They just aren’t relevant at all to the actual work i need to get done.
Surely you’ve noticed this about learning computer things, though: All the irrelevant things end up being part of some picture. It all ends up being useful toward the gestalt computer.
Yessss. You are my distraction enabler.
1 UPDATE: This is no longer true.