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

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

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

Update:

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

Ответ 1

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

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 на тех, что находятся в главном, действительно возникают эффекты.

Ответ 2

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

Ответ 3

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

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

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\"]"