Damian Nadales
Damian Nadales

Reputation: 5027

Pipes Tutorial: ListT example

I'm trying to make sense of one of the examples presented at the pipes tutorial concerning 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)

If the example above is run, we get output like the following:

>>> 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>
>>> 

It seems that:

  1. 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.
  2. 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.
  3. 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).

I'm missing something fundamental about the way the example above works, but I cannot see what.

Upvotes: 7

Views: 406

Answers (2)

mbw
mbw

Reputation: 350

To add to @danidiaz's great answer, you can get the behavior of name prompting for a firstName again, instead of just keep asking for lastNames.

What you can do is make use of the MonadZip instance of Monad m => ListT m, particularly

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

Thus, you could define name as

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

and you get your alternating behavior.

As a nice aside, you can use the MonadComprehensions and ParallelListComp extensions as well. Using these, the two versions of name become

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 ]

Upvotes: 0

danidiaz
danidiaz

Reputation: 27771

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 doesn't work that way. Instead, it is "depth-first". Every time it gets a first name, it starts reading the whole list of last names anew.

The example doesn't do that, but each list of last names could depend on the first name that has been read previously. Like this:

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.

If we are reading last names and encounter a quit command, that "branch" terminates and we go back to the level above, to read another first name from the list. The "effectful list" that reads the last names is only re-created after we have a first name to work with.

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).

Notice that entering a single quit at the very beginning will also terminate the program, as we are "closing" the top-level list of first names.

Basically, every time you enter a quit you close the current branch and go up a level in the "search tree". Every time you enter a first name you go down one level.

Upvotes: 9

Related Questions