6

抱歉,如果这是一个常见问题。我有这个简单的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从命令行编译和管道输入。IOwithinputs我在 Hoogle 中没有看到类似的东西。(withArgs是诱人的类型和命名,但根本不是我想要的。)有没有简单的方法来做到这一点?还是因为某种原因不可能?这是管道的用途吗?

4

3 回答 3

3

正如其他人所指出的那样,IO如果您已经在使用类似getLineputStrLn. 你必须修改greeter. 您可以使用hGetLineandhPutStr版本并IO使用 fake 进行模拟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,从构建 a 的人的角度来看,这些构造函数几乎就像常规函数。FakeIOF如果我们愿意,PutStr我们必须提供一个String. 如果我们愿意,GetLine我们提供一个函数,只在给定 a 时给出下一个动作String

现在我们需要一些令人困惑的样板文件。我们使用该liftF函数将我们的函子变成一个FreeT单子。请注意,我们提供了作为我们的功能的下()一个操作。事实证明,如果我们考虑自己的行为方式,这些会给我们正确的“返回值”。PutStridString -> nextFakeIO 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实例。诀窍是对于任何和f`FreeT f m都是 a 。MonadMonad mFunctor

这样就完成了我们的“模拟”greeter功能。现在我们必须以某种方式解释它,因为到目前为止我们几乎没有实现任何功能。要编写解释器,我们使用iterT函数 from Control.Monad.Trans.Free。它的完全通用类型如下

iterT
  :: (Monad m, Functor f) => (f (m a) -> m a) -> FreeT f m a -> m a

但是当我们将它应用到我们的FakeIOmonad 时,它看起来

iterT
  :: (FakeIOF (IO a) -> IO a) -> FakeIO a -> IO a

这要好得多。我们为它提供了一个函数,它将在“下一个动作”位置(这就是它的名字)中FakeIOF充满动作的函子转换为一个简单的动作,并将魔法变成真正的.IOIOiterTFakeIOIO

对于我们的默认解释器来说,这真的很容易。

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

但我们也可以制作一个 mocked 解释器。我们将使用 的工具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

现在我们可以测试这些功能

λ> 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
于 2013-11-13T14:54:03.547 回答
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
于 2013-11-13T14:53:09.537 回答
0

一旦进入,IO就无法改变获取输入的方式。要回答关于 的问题pipes,是的,可以通过定义 a 来抽象化输入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
于 2013-11-13T20:41:22.260 回答