haskell管道串入IO

时间:2013-11-13 12:17:48

标签: haskell io

很抱歉,如果这是一个常见问题。我有这个简单的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的用途吗?

3 个答案:

答案 0 :(得分:3)

正如其他人所指出的那样,如果您已经在使用IOgetLine之类的内容,则无法“模拟”putStrLn。您必须修改greeter。您可以使用hGetLinehPutStr版本并使用假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。请注意,我们将()作为PutStrid的下一个操作作为我们的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 Monadm 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