【问题标题】:Why the Haskell sequence function can't be lazy or why recursive monadic functions can't be lazy为什么 Haskell 序列函数不能惰性或递归单子函数不能惰性
【发布时间】:2013-01-24 05:34:27
【问题描述】:

通过Listing all the contents of a directory by breadth-first order results in low efficiency我了解到效率低下是由于递归单子函数的奇怪行为。

试试

sequence $ map return [1..]::[[Int]]
sequence $ map return [1..]::Maybe [Int]

而ghci会陷入无休止的计算中。

如果我们将序列函数重写为更易读的形式,如下所示:

sequence' []     = return []
sequence' (m:ms) = do {x<-m; xs<-sequence' ms; return (x:xs)}

并尝试:

sequence' $ map return [1..]::[[Int]]
sequence' $ map return [1..]::Maybe [Int]

我们遇到同样的情况,一个无限循环。

尝试一个有限列表

sequence' $ map return [1..]::Maybe [Int]

经过长时间的等待,它会弹出预期的结果Just [1,2,3,4..]

通过我们的尝试,我们可以得出结论,虽然sequence'的定义看起来很懒惰,但它是严格的,必须在打印sequence'的结果之前把所有的数字都弄出来。

不仅仅是序列',如果我们定义一个函数

iterateM:: Monad m => (a -> m a) -> a -> m [a]
iterateM f x = (f x) >>= iterateM0 f >>= return.(x:)

试试

iterateM (>>=(+1)) 0

然后进行无休止的计算。

众所周知,non-monadic iterate的定义和上面的iterateM一样,但是为什么iterate是lazy的,iterateM是strict的。 从上面我们可以看出,iterateM和sequence'都是递归monadic函数。递归monadic函数有什么奇怪的地方

【问题讨论】:

    标签: loops haskell recursion monads lazy-evaluation


    【解决方案1】:

    问题不在于sequence 的定义,而在于底层monad 的操作。尤其是monad的&gt;&gt;=操作的严格性决定了sequence的严格性。

    对于足够惰性的 monad,完全可以在无限列表上运行 sequence 并逐步使用结果。考虑:

    Prelude>  :m + Control.Monad.Identity
    Prelude Control.Monad.Identity> runIdentity (sequence $ map return [1..] :: Identity [Int])
    

    并且列表将根据需要以增量方式打印(使用)。

    Control.Monad.State.StrictControl.Monad.State.Lazy 试试这个可能很有启发性:

    -- will print the list
    Prelude Control.Monad.State.Lazy> evalState (sequence $ map return [1..] :: State () [Int]) ()
    -- loops
    Prelude Control.Monad.State.Strict> evalState (sequence $ map return [1..] :: State () [Int]) ()
    

    IO monad 中,&gt;&gt;= 根据定义是严格的,因为这种严格性正是启用关于效果排序的推理所必需的属性。我认为@jberryman 的回答很好地说明了“严格的&gt;&gt;=”的含义。对于IO 和其他具有严格&gt;&gt;= 的monad,列表中的每个表达式都必须在sequence 可以返回之前进行评估。对于无限的表达式列表,这是不可能的。

    【讨论】:

    • 我对IO的>>=是否懒惰,如果是懒惰为什么“sequence $ repeat getLine”可以逐行读取;如果严格来说“wrong = do {fileData
    • @TorosFanny 许多 Haskell IO 功能,例如hGetContents,执行惰性I/O,通过函数unsafeInterleaveIO绕过IO的bind正常操作。 hGetContents 返回一个按需评估的惰性字符串,但是由于withFile,底层句柄在任何数据被强制之前已经关闭。您可以使用unsafeInterleaveIO 进行目录遍历,但推理起来很棘手,通常不推荐。
    • 为什么使用Control.Monad.Identity解决了严格评估&gt;&gt;=的问题?
    • @DamianNadales 它与&gt;&gt;= 的实现方式的语义有关。对于身份,m &gt;&gt;= k = k (runIdentity m)runIdentity 只是解包 newtype 包装器。定义中没有任何内容需要评估 m,这意味着此 &gt;&gt;= 是非严格的。
    【解决方案2】:

    你不太了解绑定的机制:

    (>>=) :: Monad m => m a -> (a -> m b) -> m b
    

    这是一个仅适用于 3 长度列表的序列实现:

    sequence3 (ma:mb:mc:[]) = ma >>= (\a-> mb >>= (\b-> mc >>= (\c-> return [a,b,c] )))
    

    您知道我们必须如何“运行”列表中的每个“单子操作”,然后才能返回外部构造函数(即最外层的 cons,或 (:))?如果您不相信,请尝试以不同的方式实施。

    这是 monad 对 IO 有用的原因之一:当您绑定两个操作时,效果会隐含排序。

    您还必须小心使用术语“懒惰”和“严格”。 sequence 确实如此,您必须遍历整个列表才能包装最终结果,但以下操作非常好:

    Prelude Control.Monad> sequence3 [Just undefined, Just undefined, Nothing]
    Nothing
    

    【讨论】:

    • 感谢您的关心,有了您的sequence3 (ma:mb:mc:[]),我恍然大悟。
    • 你说对了一部分,但是一些 monad(Identity、State 的惰性版本等)对 bind 有惰性实现。如果您在无限列表上调用序列,这些“惰性单子”将返回。
    【解决方案3】:

    Monadic sequence 通常不能在无限列表上懒惰地工作。考虑它的签名:

    sequence :: Monad m => [m a] -> m [a]
    

    它将参数中的所有单子效果组合成一个效果。如果将其应用于无限列表,则需要将无限数量的效果组合为一个。对于某些 monad,它是可能的,对于某些 monad,它不是。

    例如,考虑sequence 专用于Maybe,就像您在示例中所做的那样:

    sequence :: [Maybe a] -> Maybe [a]
    

    如果数组中的所有元素都是Just ...,则结果是Just ...。如果任何元素为Nothing,则结果为Nothing。这意味着除非您检查输入的所有元素,否则您无法判断结果是Nothing 还是Just ...

    这同样适用于专用于[]sequencesequence :: [[a]] -&gt; [[a]]。如果参数的任何元素是一个空列表,则整个结果是一个空列表,就像在sequence [[1],[2,3],[],[4]] 中一样。因此,为了评估列表列表中的sequence,您必须检查所有元素以查看结果的样子。


    另一方面,专用于Reader monad 的序列可以延迟处理它的参数,因为Reader 的monadic 计算没有真正的“效果”。如果你定义

    inf :: Reader Int [Int]
    inf = sequence $ map return [1..]
    

    或许

    inf = sequence $ map (\x -> reader (* x)) [1..]
    

    它会懒惰地工作,你可以通过调用take 10 (runReader inf 3)看到。

    【讨论】:

    • 虽然我喜欢Maybe的解释,但这个答案不一定正确。
    • @JohnL 谢谢,你完全正确。我更正了答案。
    • 关于无限列表,请考虑 Maybe 示例。实际上,只要您在列表中遇到Nothing,就可以确定最终结果是Nothingsequence (repeat Nothing) == Nothing。如果您遇到诸如Nothing 之类的“终止效应”,则无需“检查输入的所有元素”。
    • @DanBurton 这是真的。但是如果你得到一个没有这种“终止效应”的无限列表(就像提问者所做的那样),你必须无限循环。
    • @newacct 究竟有什么不正确的?我说一般来说 monad 是不可能的,对一些人来说是,对一些人来说不是。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多