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