Потоки Haskell с эффектами ввода-вывода

рассмотрим следующую программу 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) вместо строк, но для стиля программирования, который имеет ограниченную полезность, так как я хочу, чтобы другие потоковые функции действовали на сами строки, а не на действия, которые могут произвести строку. Но, основываясь на обдумывании других ответов, я думаю, что понимаю, почему это вообще неразрешимо. В простом случае я представил, что Я действительно хотел был sequence оператор, так как я думал, что упорядочение потока подразумевало упорядочение действий. На самом деле такой порядок не обязательно подразумевается. Это становится яснее, когда я думаю о потоковой функции, которая принимает два потока в качестве входных данных (например, попарное добавление двух потоков). Если оба" входящих " потока выполняли IO, порядок этих действий IO не определен (если, конечно, мы не определяем его, упорядочивая его сами в монаде IO). Проблема решена, спасибо вам всем!

3 ответов


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

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 фактически делает любой IO, этот вместо этого создает список действий ввода-вывода для выполнения. (Примечание тип изменения подпись.) Затем основная функция выполняет 2 из этих действий, запускает их и печатает результаты:

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

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


Я действительно не понимаю вашу основную цель, но ваше использование 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 это приводит к исключению.


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

вы можете попробовать использовать что-то вроде unsafeInterleaveIO, который является как ленивый IO (например,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\"]"