Search code examples
haskellhaskell-pipes

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.


Solution

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