11

[This question is motivated by Chapter 9 in "Real World Haskell"]

Here's a simple function (cut down to essentials):

saferOpenFile path = handle (\_ -> return Nothing) $ do
      h <- openFile path ReadMode
      return (Just h)

Why do I need that $ ?

If the second argument to handle isn't a do block, I don't need it. The following works just fine:

handle (\_ -> putStrLn "Error calculating result") (print x)

When I tried removing the $ compilation failed. I can get it to work if I explicitly add parens, i.e.

saferOpenFile path = handle (\_ -> return Nothing)  (do
      h <- openFile path ReadMode
      return (Just h))

I can understand that, but I guess I'm expecting that when Haskell hits the do it should think "I'm starting a block", and we shouldn't have to explicitly put the $ there to break things up.

I also thought about pushing the do block to the next line, like this:

saferOpenFile path = handle (\_ -> return Nothing)  
  do
    h <- openFile path ReadMode
    return (Just h)

but that doesn't work without parens either. That confuses me, because the following works:

add a b = a + b

addSeven b = add 7
   b

I'm sure I'll just reach the point where I accept it as "that's just how you write idiomatic Haskell", but does anyone have any perspective to give? Thanks in advance.

Guy Coder
  • 24,501
  • 8
  • 71
  • 136
Jonathan
  • 345
  • 2
  • 8
  • Sorry, folks, I don't know stackoverflow very well. The code is just trash. (Why isn't there a "Preview Question"). I'll try to re-post it. – Jonathan Feb 06 '11 at 03:50
  • just hit the "code" button on the editor toolbar (it looks like a pair of braces). Also, preview is right below where you type, updated in real time... – Jon Feb 06 '11 at 03:52
  • 1
    Ah, NoScript needed to allow one more domain! It looks alright now. Thanks, Jon! – Jonathan Feb 06 '11 at 03:54
  • Dang, I'm making all the comments. I've played around some more, and it might just be down to my expecting Haskell to start a "block", and it just doesn't. It's probably because C is my baseline language, and the book says "The do keyword introduces a block of actions ....." – Jonathan Feb 06 '11 at 03:57

3 Answers3

10

This is due to Haskell's order of operations. Function application binds tightest, which can be a source of confusion. For example:

add a b = a + b
x = add 1 add 2 3

Haskell interprets this as: the function add applied to 1, and the function add. This is probably not what the programmer intended. Haskell will complain about expecting a Num for the second argument, but getting a function instead.

There are two solutions:

1) The expression can be parenthesized:

x = add 1 (add 2 3)

Which Haskell will interpret as: the function add applied to 1, then the value of add 2 3.

But if the nesting gets too deep, this can get confusing, hence the second solution.

2) The $ operator:

x = add 1 $ add 2 3

$ is an operator that applies a function to its arguments. Haskell reads this as: the function (add 1) applied to the value of add 2 3. Remember that in Haskell, functions can be partially applied, so (add 1) is a perfectly valid function of one argument.

The $ operator can be used multiple times:

x = add 1 $ add 2 $ add 3 4

Which solution you pick will be determined by which you think is more readable in a particular context.

Callum
  • 862
  • 5
  • 13
  • 3
    Thanks, Callum. I guessed that ($) was mostly about syntax when I tried to write a definition of it and came up with ($) f x = f x. From my limited sources it seems like Haskellers really like to avoid parens, where possible. I guess it's just hard coming from other languages, where I'm good at reading add( 1, add( 3, 4)). Especially when there are combinators being used – Jonathan Feb 06 '11 at 14:40
10

This actually isn't quite specific to do-notation. You also can't write things like print if x then "Yes" else "No" or print let x = 1+1 in x*x.

You can verify this from the grammar definition at the beginning of chapter 3 of the Haskell Report: a do expression or a conditional or a let expression is an lexp, but the argument of function application is an aexp, and an lexp is generally not valid as an aexp.

This doesn't tell you why that choice was made in the Report, of course. If I had to guess, I might surmise that it's to extend the useful rule "function application binds tighter than anything else" to the binding between, say, do and its block. But I don't know whether that was the original motivation. (I find the rejected forms like print if x then ... hard to read, but that may just be because I'm used to reading code that Haskell accepts.)

Reid Barton
  • 14,951
  • 3
  • 39
  • 49
6

According to the Haskell 2010 report, do desugars like this:

do {e;stmts} = e >>= do {stmts}

do {p <- e; stmts} = let ok p = do {stmts}
                         ok _ = fail "..."
                     in e >>= ok

For the second case, it's the same to write it desugaring like this (and easier for demonstrative purposes):

do {p <- e; stmts} = e >>= (\p -> do stmts)

So suppose you write this:

-- to make these examples shorter
saferOpenFile = f
o = openFile

f p = handle (\_ -> return Nothing) do
    h <- o p ReadMode
    return (Just h)

It desugars to this:

f p = handle (\_ -> return Nothing) (o p ReadMode) >>= (\h -> return (Just h))

Which is the same as this:

f p = (handle (\_ -> return Nothing) (o p ReadMode)) >>= (\h -> return (Just h))

Even though you probably intended for it to mean this:

handle (\_ -> return Nothing) ((o p ReadMode) >>= (\h -> return (Just h)))

tl;dr - when do syntax is desugared, it does not parenthesize the entire statement like you would think it should (as of Haskell 2010).

This answer is only AFAIK and

  1. may not be correct
  2. may be outdated someday if it is correct

Note: if you say to me "but the actual desugaring uses a 'let' statement that should group e >>= ok, right?", then I would answer the same as the tl;dr for do statements: it doesn't parenthesize/group like you think it does.

Dan Burton
  • 53,238
  • 27
  • 117
  • 198
  • Well, I guess the report makes this answer definitive. I guess I'm just having a harder time getting used to Haskell's lambda-calculus style function application that I thought I was. – Jonathan Feb 06 '11 at 14:50
  • It's really a combination of this answer and Callum's that draws the complete answer to your question. This one to explain why `$` is needed and the other to explain what it does. – Dan Burton Feb 06 '11 at 18:34
  • 2
    It's not this answer. De-sugaring always works on finished abstract syntax trees, which means it takes 'fully parenthesized' input and produces 'fully parenthesized' output. It doesn't change the definition of what valid input looks like, or when you need parentheses. (It's defined more like Lisp macros than like Lisp reader macros or C's token sequence macros). – Jonathan Cast Nov 17 '15 at 19:20