如其他人所指出的,如果你已经在使用getLine
和putStrLn
等方法,那么没有简单的方法来“模拟”IO
。你必须修改greeter
。你可以使用hGetLine
和hPutStr
版本,并用假的Handle
替换IO
,或者你可以使用使用自由单子净化代码的方法。
虽然更加复杂,但这种方法通常更加通用并适合这种类型的模拟,特别是当它变得更加复杂时。我将在下面简要解释一下,尽管细节有些复杂。
这个想法是创建自己的“假IO”单子,可以有多种方式“解释”。主要的解释就是使用常规的IO
。模拟的解释则用假行替换getLine
,并把所有内容回显到stdout
。
我们将使用free
包。第一步是用一个Functor
描述你的接口。基本概念是每个命令都是你的单子数据类型的一个分支,而单子的“位置”表示“下一步操作”。
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Trans.Free
data FakeIOF next = PutStr String next
| GetLine (String -> next)
deriving Functor
这些构造函数在构建“FakeIOF”时,从某种程度上看,就像普通的“IO”函数一样,如果忽略下一个操作。如果我们想要“PutStr”,我们必须提供一个“String”。如果我们想要“GetLine”,我们需要提供一个函数,该函数只会在给定一个“String”时返回下一个操作。
现在我们需要一些令人困惑的样板文件。我们使用“liftF”函数将我们的“functor”转换成“FreeT”单子。请注意,我们在“PutStr”上提供了“()”作为下一个操作,并提供了“id”作为我们的“String -> next”函数。事实证明,如果我们考虑我们的“FakeIO”“Monad”的行为方式,这些都可以给我们正确的“返回值”。
type FakeIO a = FreeT FakeIOF IO a
fPutStr :: String -> FakeIO ()
fPutStr s = liftF (PutStr s ())
fGetLine :: FakeIO String
fGetLine = liftF (GetLine id)
利用这些,我们可以构建任何我们想要的功能,并通过非常小的更改来重新编写greeter
。
fPutStrLn :: String -> FakeIO ()
fPutStrLn s = fPutStr (s ++ "\n")
greeter :: FakeIO ()
greeter = do
fPutStr "What's your name? "
name <- fGetLine
fPutStrLn $ "Hi, " ++ name
这可能看起来有点神奇 --- 我们使用
do
符号而没有定义
Monad
实例。诀窍在于,对于任何
Monad
m
和
Functor
f
,
FreeT f m
都是
Monad
。
这完成了我们的“模拟”
greeter
函数。现在我们必须以某种方式解释它,因为到目前为止我们几乎没有实现任何功能。要编写一个解释器,我们使用
Control.Monad.Trans.Free
中的
iterT
函数。它的完全一般类型如下:
iterT
:: (Monad m, Functor f) => (f (m a) -> m a) -> FreeT f m a -> m a
但是当我们将其应用于我们的
FakeIO
单子时,它看起来像这样。
iterT
:: (FakeIOF (IO a) -> IO a) -> FakeIO a -> IO a
这样做更加友好。我们提供了一个函数,它接受填充有IO
操作的FakeIOF
函子(位于“下一个动作”位置,因此得名)并将其转换为普通的IO
操作,iterT
将完成将FakeIO
转换为真正的IO
的魔术。
对于我们的默认解释器来说,这非常容易。
interpretNormally :: FakeIO a -> IO a
interpretNormally = iterT go where
go (PutStr s next) = putStr s >> next
go (GetLine doNext) = getLine >>= doNext
但我们也可以制作一个模拟的解释器。我们将利用IO
的功能来存储一些状态,特别是一个假响应的循环队列。
newQ :: [a] -> IO (IORef [a])
newQ = newIORef . cycle
popQ :: IORef [a] -> IO a
popQ ref = atomicModifyIORef ref (\(a:as) -> (as, a))
interpretMocked :: [String] -> FakeIO a -> IO a
interpretMocked greetings fakeIO = do
queue <- newQ greetings
iterT (go queue) fakeIO
where
go _ (PutStr s next) = putStr s >> next
go q (GetLine getNext) = do
greeting <- popQ q
putStrLn greeting
getNext greeting
现在我们可以测试这些函数。
λ> interpretNormally greeter
What's your name? Joseph
Hi, Joseph.
λ> interpretMocked ["Jabberwocky", "Frumious"] (greeter >> greeter >> greeter)
What's your name?
Jabberwocky
Hi, Jabberwocky
What's your name?
Frumious
Hi, Frumious
What's your name?
Jabberwocky
Hi, Jabberwocky
getLine
将始终从stdin
读取,这是用户键入的内容。最简单的更改方法是改用hGetLine
,它允许您指定要从哪个Handle
获取该行。也可能(我非常不确定)通过使用GHC.IO.Handle
模块进行一些低级操作并设置重定向来完成,但这显然是您想做的最后一件事。 - kqrstdin
的方法,但它不允许您这样做),但这不是pipes
库的用途。pipes
用于编写程序,其中将数据生成器与数据处理步骤分开。您可以将其与IO一起使用(通常如此),我建议使用教程。 - bheklilrIO()
参数应该处理其他Haskell函数的数据,那为什么它不接受它作为参数呢?IOwithinputs :: [String] -> ([String] -> IO()) -> IO()
。(请注意,基本上IOwithinputs ≡ flip($)
。) - leftaroundabout