How do I conditionally bind in a do block?
Asked Answered
N

2

5

I want to achieve the following in a do block:

do 
  if condition then
    n0 <- expr0
  else
    n0 <- expr0'
  n1 <- expr1
  n2 <- expr2
  return T n0 n1 n2

But Haskell gives a compile error unless I do:

do 
  if condition then
    n0 <- expr0
    n1 <- expr1
    n2 <- expr2
    return T n0 n1 n2  
  else
    n0 <- expr0'
    n1 <- expr1
    n2 <- expr2
    return T n0 n1 n2 

It looks very verbose, especially when there are many shared binding expressions. How do I make it more concise?

Actually, I am trying to do the following:

do 
  if isJust maybeVar then
    n0 <- f (fromJust maybeVar)
    n1 <- expr1
    n2 <- expr2
    return (T (Just n0) n1 n2)
  else
    n1 <- expr1
    n2 <- expr2
    return (T Nothing n1 n2)

The following still fails to compile:

do 
  n0 <- if isJust maybeVar then Just (f (fromJust maybeVar)) else Nothing
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)
Naif answered 1/9, 2019 at 19:4 Comment(2)
You wrote if ... then ... then, not if ... then ... else.Woodhouse
if isJust maybeVar then Just (f (fromJust maybeVar)) else Nothing is much better spelled f <$> maybeVar.Grain
D
12

You can "inline" the condition:

do 
  n0 <- if condition then expr0 else expr0'
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)

You likely should use brackets in the return expression, so return (T n0 n1 n2).

You can then rewrite the expression with liftM3 :: Monad m => (a1 -> a2 -> a3 -> r) -> m a1 -> m a2 -> m a3 -> m r to:

liftM3 T (if condition then expr0 else expr0') expr1 expr2

Since Haskell is a pure language evaluating expressions has no side-effects. But here the if...then...else will at most evaluate one of the two expression. An IO a itself has no side-effects either, since it is a "recipe" to generate an a.

EDIT: For your second example, it is more complicated.

do 
    n0 <- if isJust maybeVar then Just <$> (f (fromJust maybeVar)) else pure Nothing
    n1 <- expr1
    n2 <- expr2
    return (T n0 n1 n2)

So here we use pure Nothing to "wrap" Nothing in the monadic context, and Just <$> to apply Just on the value(s) inside the monadic context.

Or as @luqui says, we can here use traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b):

do 
    n0 <- traverse f maybeVar
    n1 <- expr1
    n2 <- expr2
    return (T n0 n1 n2)

This works since a Maybe is traversable: for a Just we traverse the single element, for Nothing it will return Nothing.

Druid answered 1/9, 2019 at 19:6 Comment(5)
I added more context to be more precise. Please advise on what I'm doing wrong.Naif
U just made my day. I still have to think more on why your solution works, but it definitely works like a charm!Naif
“due to Haskell's laziness” – that's a bit of a red herring. Even if they were evaluated eagerly, that still wouldn't mean they would be executed as well.Ribal
@leftaroundabout: yes I agree. I rewrote that part.Druid
BTW the second example can be written n0 <- traverse f maybeVar (though I like that you showed it the way you did for pedagogical reasons)Siouxie
A
5

Since the OP says "I still have to think more on why your solution works" in a comment, I thought I would add some explanation as a complementary answer.

Haskell's if condition then x else y syntax is actually not an analogue for the standard if/then/else statement seen in almost every imperative language. It is much more closely analogous to conditional expression syntax (seen in C as condition ? x : y, or in Python as x if condition else y). Once you remember that, everything else about it follows naturally.

if condition then x else y in Haskell is an expression for a value, not a statement. x and y are not "things to do" based on whether condition is true or not, but simply 2 different values; the whole if/then/else expression is a value that is either equivalent to x or equivalent to y (depending on the condition).

So with that in mind, lets take a look at the working version suggested by Willem Van Onsem:

do 
  n0 <- if condition then expr0 else expr0'
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)

Here the if condition then expr0 else expr0' is entirely on the right hand side of a single <- statement. So it's an expression for a value, just like expr1 and expr2 are on the following lines. It doesn't say to either bind n0 from expr0 or bind n0 from expr0', it just is either expr0 or expr0'. The <- statement that contains the if/then/else is what says to bind n0, and it binds it unconditionally from the single value computed by the whole if/then/else.

We can easily see this by the fact that we could declare a top-level variable equal to the if/then/else totally independently of the do block (assuming that condition, expr0, and expr0' are globally available) and replace the if/then/else by a reference to this variable.

foo = do 
  n0 <- z
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)

z = if condition then expr0 else expr0'

Here it's very clear that the if/then/else has nothing at all to do with the binding of n0 in the do block.

Let's compare that to the original non-working version:

do 
  if condition then
    n0 <- expr0
  else
    n0 <- expr0'
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)

This is using an if/then/else with statements as the then and else parts. Instead of just "being" one value or another, this is saying to "do" one thing or another. That's not how Haskell's if/then/else works. The whole if/then/else needs to be able to be understood as an expression for a single value.

Again this should be clear if we imagine trying to factor out the if/then/else into a separate declaration:

foo = do
  z
  n1 <- expr1
  n2 <- expr2
  return (T n0 n1 n2)

z = if condition then
      n0 <- expr0
    else
      n0 <- expr0'

It should be clear that this doesn't make any sense. The then and else parts aren't independent value-expressions, they only make sense inside a do block. And they need to be inside the particular do block in foo, so that n0 is bound for later use in return (T n0 n1 n2).

Since the statements of do blocks are transformed into expressions anyway, you might think that putting statements as the then/else parts of an if expression should work. However the transformation of do blocks into expressions "cuts across" statements, so this doesn't work. For example this:

do  n <- expr
    rest

is equivalent to this:

expr >>= (\n -> rest)

I won't get into a full technical explanation of that if you don't already understand it, but hopefully you can see that the n ends up more closely connected to rest than it does with the expr it was bound from in the statement (rest represents the entire remaining contents of the do block, in more complicated examples). There is no single expression that represents just the n <- expr part, which you could put inside the then or else part of an if/then/else expression.

Anserine answered 2/9, 2019 at 1:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.