You're dealing with two syntactic sugar constructs: a do-notation and a let block inside a do-notation. Things should become clear if we simply desugar your correct function implementation.
Original
parseAtom = do
first <- letter <|> symbol
rest <- many (letter <|> digit <|> symbol)
let atom = first:rest
return $ case atom of
"#t" -> Bool True
"#f" -> Bool False
_ -> Atom atom
Let-block desugared
parseAtom = do
first <- letter <|> symbol
rest <- many (letter <|> digit <|> symbol)
let
atom = first : rest
in do
return $ case atom of
"#t" -> Bool True
"#f" -> Bool False
_ -> Atom atom
Desugared completely
parseAtom =
(letter <|> symbol) >>= \first ->
many (letter <|> digit <|> symbol) >>= \rest ->
let
atom = first : rest
in return $ case atom of
"#t" -> Bool True
"#f" -> Bool False
_ -> Atom atom
It should also be noted that a simple let-expression inside a do-notation can be replaced with a bind-expression as you expected - all you need to do is just drop in a return
. So let atom = first:rest
can be translated to atom <- return $ first : rest
.