Chris Stryczynski
Chris Stryczynski

Reputation: 33861

Haskell do notation edge case fails to typecheck

I'm trying to understand the rules for do notation.

Here is some code that typechecks:

fff :: Maybe Int
fff = do
    _ <- pure $ Just 100
    (+10)
    <$> Just 50

Which is basically fff = (+10) <$> Just 50. I would assume the above could would not type check - because surely each line should be within the context of Maybe which (+10) is not.

Why does the above typecheck? Here is a simpler example of the above:

fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

Why is the above considered valid syntax? Does that not 'desugar' to:

fff i = ((+10) >>= (\i -> fmap (Just i))) i

Which indeed gives a typecheck error in ghci.


Here is an example that does not typecheck following a similar indentation as above:

x :: Maybe Int
x = do
  _ <- Just 1
  undefined
  <$> Just 5

(Thanks to @cvlad from the FP slack chat for the above example)

Upvotes: 3

Views: 145

Answers (2)

Will Ness
Will Ness

Reputation: 71070

fff :: Int -> Maybe Int
fff i = do
    (+10)
    <$> Just i

Why is the above considered valid syntax?

Because it is parsed as

fff i = do {        -- do { A } is just
    (+10) }         --      A
    <$> Just i

which is equivalent to

fff i =
    (+10) 
    <$> Just i

because <$> Just i on its own is an invalid expression (so fff i = ((+10) >>= (\i -> fmap (Just i))) i is incorrect translation), and that delimits the extent of the do block as per the rule quoted in @chi's answer.

Indeed its type is inferred as

fff :: Num b => b -> Maybe b

You second example works if you add a space before the <$> in the last line. Without the space, it is again parsed as

inputTest :: FormInput -> IO (Either [String] (Int, Int))
inputTest fi = do {
    allErrors' <- undefined :: IO [String]
    undefined }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

because <$> ... on its own is invalid expression. Indeed when I add the explicit separators,

inputTest2 :: String -> IO (Either [String] (Int, Int))
inputTest2 fi = do {
    allErrors2 <- undefined :: IO [String] ;
    undefined  }
    <$> ((liftM2 ) (,) <$> undefined <*> undefined) fi

I get the exact same error message on TIO (had to use String instead of your type there).

Since the first undefined :: IO [String], the whole do block has some IO t type, and we can't fmap that over anything.

Always add all the explicit separators (in addition to practicing good indentation style), to avoid this weird syntax brittleness.


Your new example is

x :: Maybe Int
x = do          -- {     this is
  _ <- Just 1   -- ;       how it is
  undefined     -- }         parsed
  <$> Just 5

The code changed, but the answer is the same. The do block before the <$> is Maybe t (because of Just 1), and we can't fmap that.

Again, indent the last line some more and it'll compile, because undefined <$> Just 5 will now be parsed as one expression.

Upvotes: 4

chi
chi

Reputation: 116139

This is a weird interaction.

I started to simplify the test case to this, which runs fine.

> x = do succ ; <$> Just 1
> x
Just 2

By comparison, this does NOT parse:

> y = do { succ ; <$> Just 1 }      
error: parse error

However, this parses:

> z = do { succ } <$> Just 1      
> z
Just 2

So, here's what I think is going on. Since token <$> can never start an expression, parse tentatively fails. The do parser rule is, essentially, a maximum munch rule: on a fail, add an implicit } and try again.

Because of this, x above is parsed as z. Since succ is a monadic value (line (+10) in OP's question) it can appear inside do. This makes type check succeed.

Quoting the Haskell Report 2.7

A close brace is also inserted whenever the syntactic category containing the layout list ends; that is, if an illegal lexeme is encountered at a point where a close brace would be legal, a close brace is inserted.

Upvotes: 4

Related Questions