'and' 子句关于懒惰的评估

Evaluation of 'and' clause with regards to laziness

我不明白为什么下面的代码会这样:

myand :: Bool -> Bool -> Bool
myand True True = True
myand _ _ = False

containsAandB :: String -> IO Bool
containsAandB s = do
  containsA <- do
    putStrLn $ "Check if 'a' is in " ++ s
    return $ 'a' `elem` s
  containsB <- do
    putStrLn $ "Check if 'b' is in " ++ s
    return $ 'b' `elem` s
  return $ containsA `myand` containsB

这是我测试函数时的输出:

*Main> containsAandB "def"
Check if 'a' is in def
Check if 'b' in in def
False

请注意 (&&) 的行为就像 'myand',我只是编写了一个自定义函数以更好地可视化发生了什么。 我对 'Check if 'b' is in def 部分感到惊讶,因为 containsA 已经是假的,所以 'myand' 可以被评估,而不管 containsB.

问题一:
containsB 也必须进行评估是否有特殊原因?我的理解是 containsB <- do ... 语句不会被评估,除非它是必需的,但我的猜测是这可能表现不同,因为它是 IO,因此不是没有副作用?

问题2:
在不处理嵌套的 if-else 子句的情况下获得所需行为的最佳实践方法是什么(如果 containsA 为假,则 containsB 未选中)?

Question 1: Is there a particular reason why containsB has to be evaluated too? My understanding was that the containsB <- do ... statement is not evaluated unless it's required but my guess is that this might behave differently since it's IO and therefore not free of side effects?

您的实验存在缺陷,因为您进行了 IOIO 的重要方面之一是尊重 IO 语句的顺序。所以即使由于懒惰,我们不需要某个值,也会执行 IO 部分。

这在 IO 的世界中是合乎逻辑的:假设我们读取一个文件,我们分为三个部分。我们读了前两部分,然后我们读了第三部分。但是现在想象一下,由于懒惰,第二个 IO 命令从未执行过。那么这意味着第三部分实际上读取了文件的第二部分。

所以简而言之由于IO,语句评估。但只有 IO 语句。所以包裹在 return 中的值是 而不是 评估的,除非你需要它。检查 'b' `elem` s 只在我们需要的时候发生。

然而,有一些方法可以“欺骗IO。例如 trace(来自 Debug.Trace)模块将执行“unsafe IO”:它会在评估时打印错误消息。如果我们写:

Prelude> import Debug.Trace
Prelude Debug.Trace> myand (trace "a" False) (trace "b" False)

我们得到了:

Prelude Debug.Trace> myand (trace "a" False) (trace "b" False)
a
False

Question2: What's the best practice approach to get the desired behavior (if containsA is false, containsB is not checked) without dealing with nested if-else clauses?

如前所述,正常行为是 containsB 评估。但是,如果您执行 IO 操作,则这些 必须在您实际进行检查之前执行 。这基本上是 IO(>>=) 运算符(您在 do 块中隐含地使用此运算符)处理的方面之一。

do 块被转换为对 >>=>> 的调用。特别是,您的代码变为(除非我遗漏了一些括号)

containsAandB s = 
  (putStrLn $ "Check if 'a' is in " ++ s >>
   return $ 'a' `elem` s) >>= (\containsA ->
  (putStrLn $ "Check if 'b' is in " ++ s >>
   return $ 'b' `elem` s) >>= (\containsB ->
  return $ containsA `myand` containsB))

所以 containsB <- do ... 并不是真正的 语句 ;它使 do ... 部分成为 >>= 调用的第一个参数。 >>=(和 >>)对于 IO 的定义总是 运行 是它的第一个参数。所以要到达最后一个 return $ ...,两个 putStrLn 调用必须已经有 运行.

这种行为不仅限于 IO monad;例如参见 Difference between Haskell's Lazy and Strict monads (or transformers)

What's the best practice approach to get the desired behavior (if containsA is false, containsB is not checked) without dealing with nested if-else clauses?

你可以一劳永逸地解决它们:

andM :: (Monad m) => m Boolean -> m Boolean -> m Boolean
andM m1 m2 = do
  x <- m1
  case x of
    True -> m2
    False -> return False

containsAandB s = andM
  (do
    putStrLn $ "Check if 'a' is in " ++ s
    return $ 'a' `elem` s)
  (do
    putStrLn $ "Check if 'b' is in " ++ s
    return $ 'b' `elem` s)

containsAandB :: String -> IO Bool
containsAandB s = do
  let containsA = do
    putStrLn $ "Check if 'a' is in " ++ s
    return $ 'a' `elem` s
  let containsB = do
    putStrLn $ "Check if 'b' is in " ++ s
    return $ 'b' `elem` s
  containsA `andM` containsB

andMhttp://hackage.haskell.org/package/extra-1.6.8/docs/Control-Monad-Extra.html 中作为 (&&^),以及其他类似的函数)。