4

The do notation allows us to express monadic code without overwhelming nestings, so that

main = getLine >>= \ a -> 
       getLine >>= \ b ->
       putStrLn (a ++ b)

can be expressed as

main = do
  a <- getLine
  b <- getLine
  putStrLn (a ++ b)

Suppose, though, the syntax allows ... #expression ... to stand for do { x <- expression; return (... x ...) }. For example, foo = f a #(b 1) c would be desugared as: foo = do { x <- b 1; return (f a x c) }. The code above could, then, be expressed as:

main = let a = #getLine in
       let b = #getLine in
       putStrLn (a ++ b)

Which would be desugared as:

main = do
  x <- getLine
  let a = x in
    return (do
      x' <- getLine
      let b = x' in
        return (putStrLn (a ++ b)))

That is equivalent. This syntax is appealing to me because it seems to offer the same functionality as the do-notation, while also allowing some shorter expressions such as:

main = putStrLn (#(getLine) ++ #(getLine))

So, I wonder if there is anything defective with this proposed syntax, or if it is indeed complete and equivalent to the do-notation.

MaiaVictor
  • 51,090
  • 44
  • 144
  • 286
  • Your last example doesn't type check. `(#(getLine) ++ #(getLine)) :: IO String` and is incompatible with `putStrLn :: String -> IO ()`. – Cirdec Apr 16 '17 at 04:58
  • Unless `#` has higher precedence than parenthesis! – Cirdec Apr 16 '17 at 05:00
  • @Cirdec how so? It desugars to `do { x <- getLine; return (do { x' <- getLine; return (putStrLn (x ++ x')) }) }`, which typechecks fine. – MaiaVictor Apr 16 '17 at 05:00
  • @Cirdec it does; I was thinking more like `#` has precedence over everything, basically giving a left-to-right sequential read direction to a block, emulating how procedural languages work. – MaiaVictor Apr 16 '17 at 05:01
  • How would you sugar nested do notation like [this](https://gist.github.com/Cedev/b55e8bba2d6a33f5023385addbf51c3e)? – Cirdec Apr 16 '17 at 05:08
  • @Cirdec replied there how I think it would look – MaiaVictor Apr 16 '17 at 05:10
  • 1
    Tangential note: the sugar-free version of your last example is `putStrLn =<< (++) <$> getLine <*> getLine`, which makes me think you might find [idiom brackets](https://personal.cis.strath.ac.uk/conor.mcbride/pub/she/idiom.html) interesting. – duplode Apr 16 '17 at 05:25
  • Or `liftA2 (++) getLine getLine`. – arrowd Apr 16 '17 at 06:24

2 Answers2

11

putStrLn is already String -> IO (), so your desugaring ... return (... return (putStrLn (a ++ b))) ends up having type IO (IO (IO ())), which is likely not what you wanted: running this program won't print anything!

Speaking more generally, your notation can't express any do-block which doesn't end in return. [See Derek Elkins' comment.]

I don't believe your notation can express join, which can be expressed with do without any additional functions:

join :: Monad m => m (m a) -> m a
join mx = do { x <- mx; x }

However, you can express fmap constrained to Monad:

fmap' :: Monad m => (a -> b) -> m a -> m b
fmap' f mx = f #mx

and >>= (and thus everything else) can be expressed using fmap' and join. So adding join would make your notation complete, but still not convenient in many cases, because you end up needing a lot of joins.

However, if you drop return from the translation, you get something quite similar to Idris' bang notation:

In many cases, using do-notation can make programs unnecessarily verbose, particularly in cases such as m_add above where the value bound is used once, immediately. In these cases, we can use a shorthand version, as follows:

m_add : Maybe Int -> Maybe Int -> Maybe Int
m_add x y = pure (!x + !y)

The notation !expr means that the expression expr should be evaluated and then implicitly bound. Conceptually, we can think of ! as being a prefix function with the following type:

(!) : m a -> a

Note, however, that it is not really a function, merely syntax! In practice, a subexpression !expr will lift expr as high as possible within its current scope, bind it to a fresh name x, and replace !expr with x. Expressions are lifted depth first, left to right. In practice, !-notation allows us to program in a more direct style, while still giving a notational clue as to which expressions are monadic.

For example, the expression:

let y = 42 in f !(g !(print y) !x)

is lifted to:

let y = 42 in do y' <- print y
                 x' <- x
                 g' <- g y' x'
                 f g'

Adding it to GHC was discussed, but rejected (so far). Unfortunately, I can't find the threads discussing it.

Community
  • 1
  • 1
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • 1
    Every `do` block that doesn't end in `return` can be trivially rewritten to a `do` block that does end in `return` that is equivalent if the monad laws hold. (Though it may lead to stack overflows as things cease to be tail calls.) – Derek Elkins left SE Apr 17 '17 at 03:40
  • You are right, this isn't sufficient. I've updated the answer. – Alexey Romanov Apr 17 '17 at 07:21
  • @AlexeyRomanov what happens if we write something like `let y = 42 in f !(g (\k -> !(print y)) !x)` in Idris? Notice the lambda. That is nonsense to me. – MaiaVictor Jul 02 '17 at 18:57
  • If it isn't clear what `!` does in some case, well, you don't have to use it. (If I understand correctly, since `print y` doesn't depend on `k`, it would be pulled out of the lambda.) – Alexey Romanov Jul 02 '17 at 19:08
2

How about this:

do a <- something
   b <- somethingElse a
   somethingFinal a b
MigMit
  • 1,698
  • 12
  • 14