这实际上是我想通过我之前的问题来理解的,但我用词不当,想当然地认为解决方案必须以
sequence
和repeat
为基础,所以我得到了一个有趣的答案,这绝对是对于解决手头的问题很有用,但我实际上也没有解决我的好奇心,所以我按照标题重新表述了这个问题。
正如您从链接的问题中看到的,我最初写的是
main :: IO ()
main = do
x <- takeWhile (/= 'q') <$> sequence (repeat getChar)
print "you pressed q"
认为
x
将是有限长度 [Char]
。
但我知道我的印象是我并没有“只是”使用了上面错误的工具(
takeWhile
,一个纯函数),但实际上根本没有(正统)工具可以从中摆脱出来IO [Char]
即sequence (repeat getChar)
。
我说得对吗?
如果没有,那么你如何摆脱困境?
注意,我现在不会问与之前相同的问题。我正在寻找一种确实利用
sequence (repeat getChar)
并且仍然设法实现 print "you pressed q"
操作的解决方案,或者解释为什么这样的解决方案不存在。
如果您
sequence
有无限的IO
操作列表,则无法通过“纯”程序逻辑来退出。所以,你是对的,没有“正统”工具可以摆脱sequence (repeat getChar)
。
摆脱困境的唯一方法是调用
IO
效果来执行“不纯”的摆脱困境,例如通过引发异常、终止线程、退出程序或类似的方式。在这些情况下,您无法轻松返回值,因此这对于您的用例来说不是很有用。或者,您可以“欺骗”并使用不安全的惰性 I/O,而不是退出,这允许您执行无限操作列表的初始前缀并检查返回列表的初始前缀。您可以无限期地继续处理列表,也可以在处理有限数量的元素后“放弃”列表。
不过,总的来说,这两种方法都不是好的做法,至少对于你的问题来说是这样。
如果你想编写一个正确的单子操作来读取字符直到第一个
'q'
,那么使用基本 Haskell 代码的正确方法是这样的:
getToQ :: IO String
getToQ = do
c <- getChar
if c == 'q' then pure [] else do
cs <- getToQ
pure (c:cs)
无法使用
sequence
来实现此逻辑的技术原因是,这是一个基本的一元算法,但 sequence
“仅”是一个应用组合器。应用组合器不能使用应用操作的结果来影响计算的“结构”。当您 sequence
IO
操作的无限列表时,您已承诺执行无限循环,并且操作的结果不能将其更改为有限循环,除非作为副作用(异常等)。特别是 sequence
无法实现 getToQ
的部分,我们检查返回值 c
并决定剩余的计算是 pure []
还是递归调用 getToQ
来执行更多操作getChar
s。您可以从以下 sequence
的(简化)定义中看到这一点:
sequence [] = pure []
sequence (x:xs) = do
a <- x
as <- sequence xs
pure (a:as)
看看从
a
操作返回的 x
值如何不能用于影响下一行中的 sequence xs
操作是否执行?这就是 sequence
在结构上与 getToQ
的不同之处。
现在,有一些包,如
extra
和 monad-loops
提供了单子组合器,可用于简洁地编写 getToQ
的等效项。例如,从 monad-loops
开始,unfoldWhileM
将完全按照您的要求进行操作:
unfoldWhileM (/= 'q') getChar
这些组合器并没有什么神奇之处。该组合器的简化实现是:
unfoldWhileM :: Monad m => (a -> Bool) -> m a -> m [a]
unfoldWhileM p x = do
a <- x
if p a then do
as <- unfoldWhileM p x
pure (a:as)
else pure []
看看这个组合器与
sequence
相比,如何成为一个非应用性的单子组合器。它使用操作 a
的返回值 x
来确定其余计算的结构: (i) 递归执行更多 x
操作;或 (ii) 结束计算。