Haskell do notation protects you from parentheses

Nested monads and function application $ do not play well with bind >>=

Starting in Haskell, I wanted to lean away from do notation, to make sure I knew what was going on under the hood before taking syntactic shortcuts.

However, I have found a small stumping block in my quest for de-sugaring. Function application $ and nested do notation results in quite clear imperative-style code. For example, E2E tests using Test.Hspec.Webdriver.

main :: IO ()
main = hspec $
  describe "E2E smoke test" $ do

    sessionWith config "integration" $ using allBrowsers $ do
      it "checks all text in p" $ runWD $ do
        openPage "http://web_test/test/files/index.html"
        e <- findElem $ ByCSS "p"
        e `shouldHaveText` "Some HTML"

      it "checks all text in p strong" $ runWD $ do
        openPage "http://web_test/test/files/index.html"
        e <- findElem $ ByCSS "p strong"
        e `shouldHaveText` "HTML"

Desugaring isn't just a case of removing do and changing the line breaks to >> or v <- expression to expression >>= \v ->. It turns out that

  • if there were nested do blocks, and multiple lines in the outer block, then you need parentheses around what was a line in the outer one;
  • if there were usages of $, then the expression to the right of $ will need to be wrapped in parentheses, and $ removed.

This is due to the facts that

  • the end of a do block, and each newline in a do block, essentially has a lower precendence than $, which in turn has a lower precedence than >>;
  • explicit lambdas, which would come from desugaring <-, take everything from -> to the end of the expression as their body.

To explain: when in a do block, the second argument of $ is from the $ to the end of the line. However, when not in a do block, the second argument of $ goes beyond the end of the line, which is ignored, beyond any >> and >>=, all the way to the end of the containing expression. Similar logic applies for the body of lambdas.

This means that the above E2E tests, but without do notation, look like the following.

main :: IO ()
main = hspec $
  describe "E2E smoke test" $

    sessionWith config "integration" $ using allBrowsers $
      (it "checks all text in p" $ runWD
        (openPage "http://web_test/test/files/index.html" >>
        findElem (ByCSS "p") >>= \e ->
        e `shouldHaveText` "Some HTML")) >>

      (it "checks all text in p strong" $ runWD
        (openPage "http://web_test/test/files/index.html" >>
        findElem (ByCSS "p strong") >>= \e ->
        e `shouldHaveText` "HTML"))

Not the end of the world of course, but certainly a point in the do-notation column for clarity, and certainly conciseness.