Учебное пособие по трубам: пример ListT

Я пытаюсь понять один из примеров, представленных на руководство по каналам относительно 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 нужно будет ввести только дважды, чтобы выйти из программы (возможно, чередуя с другим вводом).

Мне не хватает чего-то фундаментального в том, как работает приведенный выше пример, но я не могу понять, что именно.


person Damian Nadales    schedule 15.05.2017    source источник


Ответы (2)


Когда вы запустите это (на GHCi), первое имя, которое вы введете, будет привязано, и изменится только второе. Я ожидаю, что оба производителя (определенные Select input) будут по очереди (возможно, недетерминировано) при чтении ввода.

ListT так не работает. Вместо этого он «сначала в глубину». Каждый раз, когда он получает имя, он начинает читать весь список фамилий заново.

В примере этого не делается, но каждый список фамилий может зависеть от имени, которое было прочитано ранее. Так:

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)

Однократный ввод quit позволит повторно привязать первое имя. Опять же, я не понимаю, почему firstName будет привязан к первому значению, введенному пользователем.

Если мы читаем фамилии и сталкиваемся с командой quit, эта «ветвь» завершается, и мы возвращаемся на уровень выше, чтобы прочитать другое имя из списка. «Эффективный список», который считывает фамилии, воссоздается только после того, как у нас есть имя для работы.

Если ввести quit два раза подряд, программа завершится. Однако я ожидаю, что quit нужно будет ввести только дважды, чтобы выйти из программы (возможно, чередуя с другим вводом).

Обратите внимание, что однократный выход в самом начале также остановит программу, так как мы «закрываем» список имен верхнего уровня.

По сути, каждый раз, когда вы вводите quit, вы закрываете текущую ветку и поднимаетесь на уровень вверх в «дереве поиска». Каждый раз, когда вы вводите имя, вы опускаетесь на один уровень вниз.

person danidiaz    schedule 15.05.2017
comment
Дополнительное примечание: в отличие от ListT, итеративный монадный преобразователь в модуле Control.Monad.Trans.Iter пакета free позволяет вам составлять эффективные списки действий параллельно с использованием экземпляра MonadPlus. hackage.haskell.org/package/ free-4.12.4/docs/ gist.github.com/danidiaz/e9382127d1c4025b9845 - person danidiaz; 16.05.2017

Чтобы добавить к отличному ответу @danidiaz, вы можете получить поведение name, снова запрашивающее firstName, вместо того, чтобы просто продолжать запрашивать lastNames.

Что вы можете сделать, так это использовать MonadZip экземпляр Monad m => ListT m, особенно

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)

и вы получаете свое альтернативное поведение.

Кстати, вы также можете использовать расширения MonadComprehensions и ParallelListComp. Используя их, две версии 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 ]
person mbw    schedule 16.12.2017