将字符串通过Haskell管道输入IO

7

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

2
很遗憾,我认为没有改变“greeter”的可能性。getLine始终stdin读取,这是用户键入的内容。最简单的更改方法是改用hGetLine,它允许您指定要从哪个Handle获取该行。也可能(我非常不确定)通过使用GHC.IO.Handle模块进行一些低级操作并设置重定向来完成,但这显然是您想做的最后一件事。 - kqr
1
我不知道这是否可能(我最初尝试找到一种写入stdin的方法,但它不允许您这样做),但这不是pipes库的用途。 pipes用于编写程序,其中将数据生成器与数据处理步骤分开。您可以将其与IO一起使用(通常如此),我建议使用教程 - bheklilr
抱歉,我不明白这个整个想法有什么合理之处。对我来说,它听起来就像一个可怕的hack。如果那个IO()参数应该处理其他Haskell函数的数据,那为什么它不接受它作为参数呢?IOwithinputs :: [String] -> ([String] -> IO()) -> IO()。(请注意,基本上IOwithinputs ≡ flip($)。) - leftaroundabout
@leftaround- 我希望这不会是一个太可怕的黑客行为。我正在教授Haskell课程,我的学生们提交简单的IO程序,都应该是相同的。我想自动化测试,这样我就不必亲自与他们交互了。 - user2744010
@user2744010 看起来在这种情况下你可能被迫使用 shell 管道,但我希望我是错的。 - Zoey Hewll
3个回答

3

如其他人所指出的,如果你已经在使用getLineputStrLn等方法,那么没有简单的方法来“模拟”IO。你必须修改greeter。你可以使用hGetLinehPutStr版本,并用假的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”的行为方式,这些都可以给我们正确的“返回值”。
-- 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实例。诀窍在于,对于任何Monad mFunctor fFreeT 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    -- 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

现在我们可以测试这些函数。
λ> 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

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接