管道教程:ListT 示例

Pipes Tutorial: ListT example

我正在尝试理解 pipes tutorial 中关于 ListT 的示例之一:

import Pipes
import qualified Pipes.Prelude as P

input :: Producer String IO ()
input = P.stdinLn >-> P.takeWhile (/= "quit")

name :: ListT IO String
name = do
    firstName <- Select input
    lastName  <- Select input
    return (firstName ++ " " ++ lastName)

如果上面的例子是运行,我们得到如下输出:

>>> runEffect $ every name >-> P.stdoutLn
Daniel<Enter>
Fischer<Enter>
Daniel Fischer
Wagner<Enter>
Daniel Wagner
quit<Enter>
Donald<Enter>
Stewart<Enter>
Donald Stewart
Duck<Enter>
Donald Duck
quit<Enter>
quit<Enter>
>>> 

好像是:

  1. 当你 运行 这个(在 ghci 上)时,你输入的名字将被绑定,只有第二个会改变。我希望两个生产者(由 Select input 定义)将轮流(可能不确定地)读取输入。
  2. 输入一次quit将允许重新绑定名字。同样,我不明白为什么 firstName 会绑定到用户输入的第一个值。
  3. 连续输入quit两次将终止程序。但是,我希望 quit 只需输入两次即可退出程序(可能与其他输入交替)。

关于上面示例的工作方式,我遗漏了一些基本的东西,但我看不出是什么。

When you run this (on GHCi), the first name you input will get bound and only the second will change. I would expect that both producers (defined by Select input) will take turns (maybe non-deterministically) at reading the input.

ListT 这样不行。相反,它是 "depth-first"。每次获得名字时,它都会重新开始读取整个姓氏列表。

该示例没有这样做,但每个姓氏列表都可能取决于之前读取的名字。像这样:

input' :: String -> Producer String IO ()
input' msg = 
    (forever $ do 
        liftIO $ putStr msg
        r <- liftIO $ getLine
        yield r
    ) >-> P.takeWhile (/= "quit")

name' :: ListT IO String
name' = do
    firstName <- Select input
    lastName  <- Select $ input' $ "Enter a last name for " ++ firstName ++ ": "
    return (firstName ++ " " ++ lastName)

Entering quit one time will allow to re-bind the first name. Again, I fail to see why firstName will get bound to the first value entered by the user.

如果我们正在读取姓氏并遇到退出命令,则 "branch" 终止,我们返回到上面的级别,从列表中读取另一个名字。读取姓氏的 "effectful list" 只有在我们有了要使用的名字后才会重新创建。

Entering quit twice in a row will terminate the program. However, I would expect that quit only has to be entered twice to quit the program (possibly alternating with other input).

请注意,在最开始输入一个 quit 也会终止程序,因为我们是 "closing" 顶级名字列表。

基本上,每次输入 quit 时,您都会关闭当前分支并在 "search tree" 中上升一个级别。每输入一个名字,就会下降一级。

为了补充@danidiaz 的出色答案,您 可以 再次获得 name 提示 firstName 的行为,而不仅仅是继续询问lastNames.

你可以做的是利用 Monad m => ListT mMonadZip 实例,特别是

mzip :: Monad m => ListT m a -> ListT m b -> ListT m (a, b).

因此,您可以将 name 定义为

name :: ListT IO String
name = do
  (firstName, lastName) <- mzip (Select input) (Select input)
  return (firstName ++ " " ++ lastName)

你会得到你的交替行为。

另外,您还可以使用 MonadComprehensionsParallelListComp 扩展。使用这些,name 的两个版本变为

name  :: ListT IO String
name  = [ firstName ++ " " ++ lastName | firstName <- Select input
                                       , lastName  <- Select input ]

name' :: ListT IO String
name' = [ firstName ++ " " ++ lastName | firstName <- Select input
                                       | lastName  <- Select input ]