Loops 为什么Haskell序列函数可以';Don’不要懒惰,或者为什么递归一元函数可以';不要偷懒

Loops 为什么Haskell序列函数可以';Don’不要懒惰,或者为什么递归一元函数可以';不要偷懒,loops,haskell,recursion,monads,lazy-evaluation,Loops,Haskell,Recursion,Monads,Lazy Evaluation,通过这个问题,我了解到效率低下是由于递归单子函数的奇怪行为造成的 试一试 而ghci将陷入无休止的计算之中 如果我们以如下更可读的形式重写序列函数: sequence' [] = return [] sequence' (m:ms) = do {x<-m; xs<-sequence' ms; return (x:xs)} 我们得到同样的情况,一个无休止的循环 尝试一个有限的列表 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..]::Maybe [Int]
经过长时间的等待,它将跳出预期的结果
,[1,2,3,4..]

通过我们的尝试,我们可以得出这样的结论:虽然序列的定义看起来很懒,但它是严格的,必须先算出所有的数字,然后才能打印序列的结果

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

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

iterateM (>>=(+1)) 0
然后,无休止的计算发生了

我们都知道,非一元迭代器的定义与上面的迭代器一样,但是为什么迭代器是惰性的,而迭代器是严格的。
从上面我们可以看到,iterateM和sequence都是递归一元函数。递归一元函数有什么奇怪的地方吗

(>>=) :: 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,或
(:)
)之前,我们必须“运行”列表中的每个“一元操作”吗?如果你不相信,试着用不同的方式实现它

这是单子对IO有用的一个原因:绑定两个动作时,效果的顺序是隐式的

您还必须小心使用术语“懒惰”和“严格”。对于
sequence
来说,在包装最终结果之前,您必须遍历整个列表,但以下操作非常有效:

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

一元
序列
通常不能在无限列表上懒洋洋地工作。考虑其签名:

sequence :: Monad m => [m a] -> m [a]
它将论证中的所有一元效应合并为一个效应。如果将其应用于无限列表,则需要将无限多个效果合并为一个。对于某些单子,它是可能的,对于某些单子,它不是

作为例子,考虑<代码>序列<代码>专门针对<代码>可能,正如您在示例中所做的:

sequence :: [Maybe a] -> Maybe [a]
如果数组中的所有元素都是
Just…
,则结果是
Just…
。如果任何元素为
Nothing
,则结果为
Nothing
。这意味着除非检查输入的所有元素,否则无法判断结果是
还是
只是…

这同样适用于专用于
[]
序列:[[a]]->[[a]]
序列。如果参数的任何元素为空列表,则整个结果为空列表,如
序列[[1]、[2,3]、[4]]
。因此,为了对列表列表中的
sequence
进行评估,您必须检查所有元素以查看结果


另一方面,专门用于
读取器
单子的序列可以缓慢地处理其参数,因为对
读取器
的单子计算没有真正的“影响”。如果你定义

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

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

它将缓慢地工作,正如您通过调用
take 10(runReader inf 3)
可以看到的,问题不在于
序列的定义,而在于底层monad的操作。尤其是monad的
>=
操作的严格性决定了
序列的严格性

对于足够懒惰的monad,完全可以在无限列表上运行
sequence
,并以增量方式使用结果。考虑:

Prelude>  :m + Control.Monad.Identity
Prelude Control.Monad.Identity> runIdentity (sequence $ map return [1..] :: Identity [Int])
列表将根据需要以增量方式打印(使用)

使用
Control.Monad.State.Strict
Control.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中,
>=
根据定义是严格的,因为这种严格性正是对效果排序进行推理所必需的属性。我认为@jberryman的回答很好地说明了“严格的
>>=
”的含义。对于
IO
和其他具有严格
>=
的monad,必须先计算列表中的每个表达式,然后才能返回
序列。由于表达式列表无限,这是不可能的。

虽然我喜欢用
Maybe
来解释,但这个答案不一定正确。@JohnL谢谢,你完全正确。我纠正了答案。关于无限列表,考虑可能的例子。事实上,只要在列表中遇到
Nothing
,您就可以确定最终结果是
Nothing
<代码>序列(无重复)=无
。如果遇到“终止效应”,例如
Nothing
@DanBurton,则不需要“检查输入的所有元素”,这是真的。但是如果你得到一个没有这种“终止效应”的无限列表(就像询问者那样),你必须无限循环。@newacct什么是不正确的?我说单子一般来说是不可能的,有些是,有些不是。谢谢你的关心,对于你的sequence3(ma:mb:mc:[]),我突然意识到了这一点。你说得有一部分是对的,但是有些单子(身份,状态的惰性版本,等等)有一个绑定的惰性实现。如果调用无限列表上的序列,这些“懒惰的单子”将返回。我不清楚IO的>>=是否懒惰,如果它懒惰,为什么“sequence$repeat getLine”可以逐行读取;如果严格解释常见错误是“错误=do{fileData@TorosFanny许多Haskell IO函数,例如,
hGetContents
,执行惰性I/O,这会通过使用函数
unsafeInterleaveIO
hGetContents
返回一个为eval的惰性字符串来规避IO绑定的正常操作
Prelude>  :m + Control.Monad.Identity
Prelude Control.Monad.Identity> runIdentity (sequence $ map return [1..] :: Identity [Int])
-- 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]) ()