Ленивая версия 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, пока хвост все еще строится.

Ответ 1

Не используйте 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 вызывает проблемы, прочитайте оригинальные слайды Олега по этому вопросу, которые вы можете найти здесь здесь. Он отлично справляется с объяснением проблем с использованием ленивого ввода-вывода. Каждый раз, когда вы вынуждены использовать ленивый IO, вам действительно нужна библиотека iteratee.

Ответ 2

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