如何在 do 块中有条件地绑定?

How do I conditionally bind in a do block?

我想在 do 块中实现以下目标:

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

但是 Haskell 给出编译错误,除非我这样做:

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 

它看起来很冗长,尤其是当有很多共享绑定表达式时。如何让它更简洁?

实际上,我正在尝试执行以下操作:

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)

以下仍然编译失败:

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

您可以“内联”条件:

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

您可能应该在 return 表达式中使用方括号,因此 return (T n0 n1 n2).

然后您可以将 liftM3 :: Monad m => (a1 -> a2 -> a3 -> r) -> m a1 -> m a2 -> m a3 -> m r 的表达式重写为:

<b>liftM3</b> T (if condition then expr0 else expr0') expr1 expr2

因为 Haskell 是一种纯语言,计算表达式没有副作用。但是这里 if...then...else 最多会计算两个表达式之一。 IO a 本身也没有副作用,因为它是 "recipe" 来生成 a.

编辑:对于你的第二个例子,它更复杂。

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

所以这里我们在 monadic 上下文中使用 pure Nothing 到 "wrap" Nothing,并且 Just <$>Just 应用于内部的值monadic 上下文。

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)

这是可行的,因为 Maybe 是可遍历的:对于 Just 我们遍历单个元素,对于 Nothing 它将 return Nothing.

由于 OP 在评论中说 "I still have to think more on why your solution works",我想我会添加一些解释作为补充答案。

Haskell 的 if condition then x else y 语法实际上 而不是 几乎所有命令式语言中的标准 if/then/else 语句的模拟。它更类似于条件表达式语法(在 C 中视为 condition ? x : y,或在 Python 中视为 x if condition else y)。一旦你记住了这一点,其他一切就会自然而然地发生。

Haskell 中的

if condition then x else y 的表达式,而不是 语句 xy 不是 "things to do" 基于 condition 是否为真,而只是两个不同的值;整个 if/then/else 表达式是一个等于 x 或等于 y 的值(取决于条件)。

考虑到这一点,让我们看一下 Willem Van Onsem 建议的工作版本:

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

此处 if condition then expr0 else expr0' 完全位于单个 <- 语句的右侧。所以它是一个值的表达式,就像下面几行的 expr1expr2 一样。它没有说从 expr0 绑定 n0 或从 expr0' 绑定 n0,它只是 或者 expr0expr0'。包含 if/then/else 的 <- 语句表示要绑定 n0,它从 single[无条件 绑定 =86=] 整体计算的值 if/then/else.

我们可以很容易地看到这一点,因为我们可以完全独立于 do 块声明一个等于 if/then/else 的顶级变量(假设 conditionexpr0 , 和 expr0' 是全局可用的)并用对此变量的引用替换 if/then/else。

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

z = if condition then expr0 else expr0'

这里很明显 if/then/else 与 do 块中 n0 的绑定完全无关。

让我们将其与原始的非工作版本进行比较:

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

这是使用 if/then/else 和 语句 作为 then 和 else 部分。这不仅仅是 "being" 一个值或另一个值,而是对 "do" 说一件事或另一件事。 Haskell 的 if/then/else 不是这样工作的。整个 if/then/else 需要能够理解为单个值的表达式。

同样,如果我们想象尝试将 if/then/else 分解为单独的声明,这应该很清楚:

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

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

应该清楚,这没有任何意义。 then 和 else 部分不是独立的值表达式,它们只在 do 块内有意义。而且它们需要在 particular do 块内 foo,这样 n0 就可以稍后在 return (T n0 n1 n2) 中使用。

由于 do 块的语句无论如何都会转换为表达式,您可能认为将语句作为 if 表达式的 then/else 部分 应该 有效。然而,将 do 块转换为表达式 "cuts across" 语句,所以这是行不通的。例如:

do  n <- expr
    rest

相当于:

expr >>= (\n -> rest)

如果您还不了解,我不会对此进行完整的技术解释,但希望您能看到 n 最终与 rest 的联系比它使用它在语句中绑定的 expr (在更复杂的示例中,rest 表示 do 块的全部剩余内容)。没有单独的表达式只表示 n <- expr 部分,您可以将其放在 if/then/else 表达式的 then 或 else 部分中。