在 Haskell 中永远理解

Understanding forever in Haskell

目标:获取一个字符串并在控制台中打印相同的内容,就像需要永远完成相同的操作一样

我想到了这样的东西,没有编译错误,但没有按预期工作。

greet_buggy :: String -> IO ()
greet_buggy = forever $ putStrLn

问题是下面的字符串没有在控制台中打印任何内容。

greet_buggy "something"

基于this post 我尝试调试并更改了如下定义。它工作正常

greet :: IO ()
greet = forever $ putStrLn "Hello"

谁能解释一下这是怎么回事?单独使用 forever 是否可以达到同样的效果?

编辑:找到了一个更相关的 post(感谢@Daniel Wagner),即使这个问题与我的不同,答案也解释了 forever

Can anyone explain whats going on here?

我们有

putStrLn :: String -> IO ()

顺便说一下,它是 (->) String monad 中的一个值。因此,

forever putStrLn :: (->) String b

其中 b 是普遍量化的,因此我们也有(不幸的是)

forever putStrLn :: (->) String (IO ())

对发布的代码进行类型检查。

要了解 (->) String monad 中的 forever 是什么,请回忆一下:

m >>= g
=  -- definition of >>=
\x -> g (m x) x

因此

m >> f
= -- definition of >> in terms of >>=
m >>= const f
= -- definition of >>=
\x -> const f (m x) x
= -- beta
\x -> f x
= -- eta
f

回到forever:回顾它的定义

forever m = m >> forever m

等同于(在 (->) String monad 中)递归定义

forever m = forever m

导致无用的无限循环。

forever 一遍又一遍地执行完全相同的 IO 操作(或者一般来说,monadic 操作)。如果您的函数接受字符串作为输入,forever 将永远打印相同的字符串。相反,如果您想每次都从用户那里读取一个字符串,则必须将其包含在您的 IO 操作中。

从用户那里读取一行然后打印出来的简单 IO 动作是什么?合并 getLineputStrLn:

echo :: IO ()
echo = getLine >>= putStrLn

然后将该操作传递给 forever:

cat :: IO ()
cat = forever echo

或者,事实证明,Prelude 中已经内置了一些东西,可以为您完成这一切:我们所写的相当于:

cat :: IO ()
cat = interact id

greet_buggy 正在处理不同的 monad。

forever 采取单子动作并无限期地重复它。可以这样定义:

forever a = let loop = a >> loop in loop

也可以形象化为

forever a = a >> a >> a >> a >> ...  (infinitely many times)

(事实上永远是在 Applicative 上定义的,而不是 Monad;但这现在并不重要)。

所以 forever greet 实际上是

putStrLn "Hello" >> putStrLn "Hello" >> putStrLn "Hello" >> ...

无需进一步解释。

OTOH forever greet_buggy 等同于

putStrLn >> putStrLn >> putStrLn >> ...

现在,由于 (-> a) 是一个 monad,>> 是为任何两个合适类型的函数定义的,在这种情况下 f >> g 的含义是......只是 g!所以

的意思
( putStrLn >> putStrLn >> putStrLn >> ... ) "Hello"

就是把>>链中的最后一个函数取到Hello上。当然那里没有 last 功能,所以这只会永远保持 运行。