Ленивая версия mapM

Предположим, я получаю большой список элементов при работе с IO:

as <- getLargeList

Сейчас я пытаюсь подать заявку fn :: a -> IO b на as:

as <- getLargeList
bs <- mapM fn as

mapM имеет тип mapM :: Monad m => (a -> m b) -> [a] -> m [b]и это то, что мне нужно с точки зрения соответствия типов. Но он строит всю цепочку в памяти, пока не вернет результат. Я ищу аналог mapM, который будет работать лениво, так что я могу использовать главу bs в то время как хвост все еще строит.

2 ответа

Решение

Не использовать unsafeInterleaveIO или любой ленивый IO в этом отношении. Это именно та проблема, для решения которой были созданы итераторы: избегать ленивых операций ввода-вывода, что приводит к непредсказуемому управлению ресурсами. Хитрость заключается в том, чтобы никогда не создавать список и постоянно транслировать его с помощью итераторов, пока вы не закончите его использовать. Я буду использовать примеры из моей собственной библиотеки, pipes, чтобы продемонстрировать это.

Сначала определите:

import Control.Monad
import Control.Monad.Trans
import Control.Pipe

-- Demand only 'n' elements
take' :: (Monad m) => Int -> Pipe a a m ()
take' n = replicateM_ n $ do
    a <- await
    yield a

-- Print all incoming elements
printer :: (Show a) => Consumer a IO r
printer = forever $ do
    a <- await
    lift $ print a

Теперь давайте будем жестоко относиться к нашим пользователям и потребовать, чтобы они составили для нас действительно большой список:

prompt100 :: Producer Int IO ()
prompt100 = replicateM_ 1000 $ do
    lift $ putStrLn "Enter an integer: "
    n <- lift readLn
    yield n

Теперь давайте запустим это:

>>> runPipe $ printer <+< take' 1 <+< prompt100
Enter an integer:
3<Enter>
3

Он запрашивает только одно целое число, поскольку мы требуем только одно целое число!

Если вы хотите заменить prompt100 с выводом из getLargeListВы просто пишете:

yourProducer :: Producer b IO ()
yourProducer = do
    xs <- lift getLargeList
    mapM_ yield xs

... а затем запустить:

>>> runPipe $ printer <+< take' 1 <+< yourProducer

Это будет лениво выполнять потоковую передачу списка и никогда не создавать список в памяти, все без использования небезопасных IO хаки. Чтобы изменить количество требуемых элементов, просто измените значение, которое вы передаете take'

Для большего количества примеров, подобных этому, прочитайте pipes учебник в Control.Pipe.Tutorial,

Чтобы узнать больше о том, почему ленивый ввод-вывод вызывает проблемы, прочитайте оригинальные слайды Олега на эту тему, которые вы можете найти здесь. Он отлично объясняет проблемы с использованием ленивых операций ввода-вывода. Каждый раз, когда вы чувствуете необходимость использовать ленивый ввод-вывод, вам действительно нужна библиотека с повторным доступом.

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

import System.IO.Unsafe

lazyMapM :: (a -> IO b) -> [a] -> IO [b]
lazyMapM f [] = return []
lazyMapM f (x:xs) = do y <- f x
                       ys <- unsafeInterleaveIO $ lazyMapM f xs
                       return (y:ys)

Вот как реализован ленивый ввод-вывод. Небезопасно ощущение, что порядок, в котором эффекты будут фактически выполняться, трудно предсказать, и он будет определяться порядком, в котором оцениваются элементы списка результатов. По этой причине важно, чтобы любые эффекты IO в f доброкачественные, в том смысле, что они должны быть нечувствительны к порядку. Хорошим примером обычно достаточно мягкого эффекта является чтение из файла только для чтения.

Другие вопросы по тегам