很抱歉,如果这是一个常见问题。我有这个简单的IO()
函数:
greeter :: IO()
greeter = do
putStr "What's your name? "
name <- getLine
putStrLn $ "Hi, " ++ name
现在我想拨打greeter
,同时指定一个预先填充getLine
的参数,这样我就不需要进行互动。我想像一个函数
IOwithinputs :: [String] -> IO() -> IO()
然后我做
IOwithinputs ["Buddy"] greeter
会产生一个IO
动作,不需要用户输入,如下所示:
What's your name?
Hi, Buddy
我想在不修改原始IO()
函数greeter
的情况下执行此操作。我也不想从命令行编译greeter
和管道输入。我在Hoogle中看不到IOwithinputs
这样的内容。 (withArgs
引人入胜地命名和命名,但根本不是我想要的。)有没有一种简单的方法可以做到这一点?或者由于某种原因它是不可能的?这是Pipes的用途吗?
答案 0 :(得分:3)
正如其他人所指出的那样,如果您已经在使用IO
和getLine
之类的内容,则无法“模拟”putStrLn
。您必须修改greeter
。您可以使用hGetLine
和hPutStr
版本并使用假IO
模拟Handle
,也可以使用Purify Code with Free Monads方法。
它更复杂,但也更通用,通常非常适合这种嘲弄,特别是当它变得更复杂时......我将在下面简要解释一下,虽然细节有些复杂。
这个想法是你将创建自己的“假IO”monad,可以通过多种方式“解释”。主要解释是使用常规IO
。模拟的解释用一些假线代替getLine
并将所有内容回显到stdout
。
我们将使用free
包。第一步是使用Functor
描述您的界面。基本概念是每个命令都是函子数据类型的一个分支,而函子的“槽”代表“下一个动作”。
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Trans.Free
data FakeIOF next = PutStr String next
| GetLine (String -> next)
deriving Functor
如果您忽略下一个操作,那么从构建IO
的某个人的角度来看,这些构造函数几乎就像常规FakeIOF
函数一样。如果我们想要PutStr
,我们必须提供String
。如果我们想要GetLine
我们提供的函数仅在给定String
时给出下一个操作。
现在我们需要一个令人困惑的样板。我们使用liftF
函数将我们的仿函数转换为FreeT
monad。请注意,我们将()
作为PutStr
和id
的下一个操作作为我们的String -> next
函数。事实证明,如果我们考虑FakeIO
Monad
的行为方式,这些会给我们正确的“回报值”。
-- Our 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
表示法。诀窍是FreeT f m
对于任何Monad
Monad
和m
f`都是Functor
。
这完成了我们的“模拟”greeter
功能。现在我们必须以某种方式解释它,因为到目前为止我们几乎没有实现任何功能。要编写解释器,我们使用iterT
中的Control.Monad.Trans.Free
函数。它的完全通用类型如下
iterT
:: (Monad m, Functor f) => (f (m a) -> m a) -> FreeT f m a -> m a
但是当我们将它应用到FakeIO
monad时,它看起来像
iterT
:: (FakeIOF (IO a) -> IO a) -> FakeIO a -> IO a
更好。我们为它提供了一个函数,它将FakeIOF
个充满IO
个动子的仿函数放在“下一个动作”位置(这就是它的名字),以及IO
动作和{{1将iterT
变为真实的FakeIO
。
对于我们的默认解释器,这非常简单。
IO
但我们也可以做一个模拟的翻译。我们将使用interpretNormally :: FakeIO a -> IO a
interpretNormally = iterT go where
go (PutStr s next) = putStr s >> next -- next :: IO a
go (GetLine doNext) = getLine >>= doNext -- doNext :: String -> IO a
的工具来存储某些状态,特别是虚假响应的循环队列。
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 -- first we pop a fresh greeting
putStrLn greeting -- then we print it
getNext greeting -- finally we pass it to the next IO action
答案 1 :(得分:2)
我不认为这样做很容易,但你可以做下一步:
greeter' :: IO String -> IO()
greeter' ioS = do
putStr "What's your name? "
name <- ioS
putStrLn $ "Hi, " ++ name
greeter :: IO ()
greeter = greeter' getLine
ioWithInputs :: Monad m => [a] -> (m a -> m ()) -> m()
ioWithInputs s ioS = mapM_ (ioS.return) s
并测试它:
> ioWithInputs ["Buddy","Nick"] greeter'
What's your name? Hi, Buddy
What's your name? Hi, Nick
,仿效答案更有趣:
> ioWithInputs ["Buddy","Nick"] $ greeter' . (\s -> s >>= putStrLn >> s)
What's your name? Buddy
Hi, Buddy
What's your name? Nick
Hi, Nick
答案 2 :(得分:0)
进入IO
后,您无法改变输入方式。要回答关于pipes
的问题,是的,可以通过定义Consumer
来抽象输入:
import Pipes
import qualified Pipes.Prelude as P
greeter :: Consumer String IO ()
greeter = do
lift $ putStr "What's your name? "
name <- await
lift $ putStrLn $ "Hi, " ++ name
然后您可以指定使用命令行作为输入:
main = runEffect $ P.stdinLn >-> greeter
...或使用一组纯字符串作为输入:
main = runEffect $ each ["Buddy"] >-> greeter