【问题标题】:Evaluation of 'and' clause with regards to laziness关于懒惰的“和”条款的评估
【发布时间】:2018-06-10 11:17:20
【问题描述】:

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

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 已经是错误的,因此无论containsB 如何都可以评估“myand”。

问题 1:
containsB 也必须被评估是否有特殊原因?我的理解是,除非需要,否则不会评估 containsB &lt;- do ... 语句,但我的猜测是,它的行为可能会有所不同,因为它是 IO,因此不会没有副作用?

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

【问题讨论】:

  • 好吧,你把它包装在一个 IO monad 中,所以由于 monad。
  • 好的,所以我的假设是正确的,因为它没有副作用,所以它的行为方式是正确的?很抱歉,这条评论有点含糊,我无法理解。
  • 想象一下你的两个元素 containsAcontainsB 从一个文件中读取。然后他们推进光标。现在这意味着根据containsA 的值,光标会(或不会)前进?这是人们通常想要避免的行为。因此,如果 IO 动作在 monad 中,它们就可以保证发生。

标签: haskell lazy-evaluation


【解决方案1】:

问题 1: 是否有特殊原因也必须评估 containsB ?我的理解是 containsB

您的实验存在缺陷,因为您执行了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

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

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

【讨论】:

  • 明白了。谢谢!
  • 术语全错了。没有“IO monad”。 IO 类型本身是一个单子(IO 也是一个函子)。当你有一个 do 块时,那不是一个单子;这只是 IO something 类型的值。
  • @melpomene:不是真的,do 块可以导致任何Monad m =&gt; m something。只有类型检查才能将其转换为特定的m。如果有的话,例如liftM2 h f g = do {x &lt;- f; y &lt;- g; return h x y} 的类型为Monad m =&gt; (a -&gt; b -&gt; c) -&gt; m a -&gt; m b -&gt; m c
  • 是的,我说的是 do 块专门做 I/O。但即使在一般情况下,do 块本身也不是“单子”; “monad”部分具体是类型签名中的m
  • @melpomene:是的,我知道,但是谈论单子有点“混乱”。所以通常在与朋友、同事等讨论时。我使用“快捷方式”,虽然同事们都理解这些,但我想这可能会让人困惑:)。
【解决方案2】:

do 块被转换为对&gt;&gt;=&gt;&gt; 的调用。特别是,您的代码变为(除非我错过了一些括号)

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 &lt;- do ... 并不是真正的声明;它使do ... 部分成为&gt;&gt;= 调用的第一个参数。并且为IO 定义了&gt;&gt;=(和&gt;&gt;),因此它总是运行它的第一个参数。所以要到达最后一个return $ ...,两个putStrLn 调用都必须已经运行。

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

在不处理嵌套 if-else 子句的情况下获得所需行为(如果 containsA 为 false,则不检查 containsB)的最佳实践方法是什么?

您可以一劳永逸地处理它们:

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 中作为(&amp;&amp;^),以及其他类似功能)。

【讨论】:

    猜你喜欢
    • 2019-01-07
    • 1970-01-01
    • 2015-09-06
    • 2016-12-14
    • 2021-12-25
    • 2018-04-01
    • 2018-03-31
    • 1970-01-01
    • 2018-06-16
    相关资源
    最近更新 更多