Loops 为什么Haskell序列函数可以';Don’不要懒惰,或者为什么递归一元函数可以';不要偷懒
通过这个问题,我了解到效率低下是由于递归单子函数的奇怪行为造成的 试一试 而ghci将陷入无休止的计算之中 如果我们以如下更可读的形式重写序列函数: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] 经过长时间
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]) ()