Потоки на Haskell с эффектами ввода-вывода - PullRequest
11 голосов
/ 16 апреля 2009

Рассмотрим следующую программу на Haskell. Я пытаюсь программировать в «потоковом стиле», где функции работают с потоками (реализованы здесь просто в виде списков). Такие вещи, как normalStreamFunc, отлично работают с ленивыми списками. Я могу передать бесконечный список в normalStreamFunc и эффективно получить другой бесконечный список, но с функцией, сопоставленной с каждым значением. Такие вещи, как effectfulStreamFunc, работают не так хорошо. Действие ввода-вывода означает, что мне нужно оценить весь список, прежде чем я смогу извлечь отдельные значения. Например, вывод программы такой:

a
b
c
d
"[\"a\",\"b\"]"

но мне нужен способ написать effectfulStreamFunc, чтобы программа выдала следующее:

a
b
"[\"a\",\"b\"]"

оставляя оставшиеся действия без оценки. Я могу представить решение, использующее unsafePerformIO, но, скажем, я убираю это со стола. Вот программа:

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = do
    putStrLn x
    rest <- effectfulStreamFunc xs
    return (reverse(x):rest)

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

Обновление:

Спасибо всем за полезные и вдумчивые отзывы. Я раньше не видел оператора sequence, о котором полезно знать. Я думал о (менее элегантном) способе передачи значений IO (String) вместо Strings, но для стиля программирования, который имеет ограниченную полезность, так как я хочу, чтобы другие функции потока действовали на сами строки, а не на действия, которые могут произвести строку. Но, исходя из обдумывания других ответов, я думаю, что понимаю, почему это вообще неразрешимо. В простом случае, который я представил, я действительно хотел оператор sequence, поскольку я думал, что упорядочение потока подразумевает упорядочение действий. На самом деле такой порядок не подразумевается. Это становится более понятным для меня, когда я думаю о функции потока, которая принимает два потока в качестве входных данных (например, попарно добавление двух потоков). Если оба «входящих» потока выполнили IO, порядок этих действий IO не определен (если, конечно, мы не определяем его, упорядочивая его в монаде IO). Проблема решена, спасибо всем!

Ответы [ 3 ]

7 голосов
/ 16 апреля 2009

Как насчет этого кода:

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> [IO (String)]
effectfulStreamFunc [] = []
effectfulStreamFunc (x:xs) =
    let rest = effectfulStreamFunc xs in
        (putStrLn x >> return x) : rest

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     let appliedFns = effectfulStreamFunc fns
     pieces <- sequence $ take 2 appliedFns
     print $ show $ pieces

Вместо effectfulStreamFunc фактически выполняет какой-либо ввод-вывод, вместо этого создается список действий ввода-вывода, которые необходимо выполнить. (Обратите внимание на изменение сигнатуры типа.) Затем основная функция выполняет 2 из этих действий, выполняет их и печатает результаты:

a
b
"[\"a\",\"b\"]"

Это работает, потому что тип IO (String) - это просто функция / значение, подобное любой другой, которую вы можете поместить в список, передать и т. Д. Обратите внимание, что синтаксис do не встречается в "effectfulStreamFunc" - это на самом деле чистая функция, несмотря на "IO" в своей подписи. Только когда мы запускаем sequence на тех, кто в основном, эффекты действительно происходят.

2 голосов
/ 16 апреля 2009

Я не совсем понимаю вашу главную цель, но использование putStrLn приводит к оценке всего списка, потому что он будет оценивать аргумент при выполнении. Рассмотрим

import IO

normalStreamFunc :: [String] -> [String]
normalStreamFunc (x:xs) = reverse(x) : normalStreamFunc xs

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = do
    rest <- effectfulStreamFunc xs
    return (reverse(x):rest)

main :: IO ()
main = do
     let fns = ["a", "b", undefined,"c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

это приводит к "[\" a \ ", \" b \ "]", а при использовании версии putStrLn это приводит к исключению.

1 голос
/ 16 апреля 2009

Как упомянул Том, вы не можете сделать это "безопасно", потому что нарушаете прозрачность ссылок в Haskell. Вы пытаетесь выполнять побочные эффекты лениво, но суть лени в том, что вам не гарантируется, в каком порядке или оцениваются ли вещи, поэтому в Haskell, когда вы говорите, что он выполняет побочный эффект, это всегда выполняется, и в точном указанном порядке. (т.е. в этом случае побочные эффекты от рекурсивного вызова effectfulStreamFunc до return, потому что это был порядок, в котором они были перечислены) Вы не можете сделать это лениво, не используя небезопасный.

Вы можете попробовать использовать что-то вроде unsafeInterleaveIO, то есть как ленивый ввод-вывод (например, hGetContents) реализован в Haskell, но у него есть свои проблемы; и вы сказали, что не хотите использовать «небезопасные» вещи.

import System.IO.Unsafe (unsafeInterleaveIO)

effectfulStreamFunc :: [String] -> IO [String]
effectfulStreamFunc [] = return []
effectfulStreamFunc (x:xs) = unsafeInterleaveIO $ do
    putStrLn x
    rest <- effectfulStreamFunc xs
    return (reverse x : rest)

main :: IO ()
main = do
     let fns = ["a", "b", "c", "d"]
     es <- effectfulStreamFunc fns
     print $ show $ take 2 es

В этом случае вывод выглядит следующим образом

"a
[\"a\"b
,\"b\"]"
...