Использует ли mapM/последовательность, считающуюся хорошей практикой?

Рассмотрим следующий пример:

safeMapM f xs = safeMapM' xs []
    where safeMapM' []     acc = return $ reverse acc
          safeMapM' (x:xs) acc = do y <- f x
                                    safeMapM' xs (y:acc)

mapM return largelist      -- Causes stack space overflow on large lists
safeMapM return largelist  -- Seems to work fine

Использование mapM в больших списках приводит к переполнению пространства стека, а safeMapM работает нормально (с использованием GHC 7.6.1 с -O2). Однако я не смог найти функцию, похожую на safeMapM в стандартных библиотеках Haskell.

До сих пор считается хорошей практикой использовать mapM (или sequence, если на то пошло)?
Если да, то почему это считается хорошей практикой, несмотря на опасность?
Если нет, какую альтернативу вы предлагаете использовать?

Ответы

Ответ 1

Как Никлас Б., семантика mapM - это эффективная правая складка, и она в большинстве случаев прекращается, чем перевернутая версия. В общем, mapM имеет больше смысла, так как редко бывает, что мы захотим сделать карту, соответствующую результатам, в огромном списке данных. Чаще всего мы хотим оценить такой список для эффектов, и в этом случае mapM_ и sequence_, которые выбрасывают результаты, обычно рекомендуются.

Изменить: другими словами, несмотря на вопрос, поднятый в вопросе, да, mapM и sequence обычно используются и обычно считаются хорошей практикой.

Ответ 2

Если да, то почему считается хорошей практикой, несмотря на опасность переполнения стекового пространства? Если нет, какую альтернативу вы предлагаете использовать?

Если вы хотите обработать элементы списка по мере их создания, используйте либо pipes, либо conduit. Оба никогда не будут создавать промежуточный список.

Я покажу путь pipes, так как это моя библиотека. Сначала я начну с бесконечного списка чисел, сгенерированных в монаде IO из пользовательского ввода:

import Control.Proxy

infiniteInts :: (Proxy p) => () -> Producer p Int IO r
infiniteInts () = runIdentityP $ forever $ do
    n <- lift readLn
    respond n

Теперь я хочу напечатать их по мере их создания. Это требует определения обработчика нисходящего потока:

printer :: (Proxy p) => () -> Consumer p Int IO r
printer () = runIdentityP $ forever $ do
    n <- request ()
    lift $ print n

Теперь я могу подключить Producer и Consumer с помощью (>->) и запустить результат с помощью runProxy:

>>> runProxy $ infiniteInts >-> printer
4<Enter>
4
7<Enter>
7
...

Затем он будет читать Int от пользователя и отсылать их обратно в консоль по мере их создания без сохранения более одного элемента в памяти.

Итак, обычно, если вы хотите, чтобы эффективное вычисление генерировало поток элементов и немедленно их уничтожало, вы не хотите mapM. Используйте соответствующую поточную библиотеку.

Если вы хотите узнать больше о pipes, я рекомендую прочитать учебник.

Ответ 3

Если вы хотите остаться в ленивом лагере, пакет lazyio позволяет обрабатывать список ввода лениво. Вместо

mapM f

вы пишете

import qualified System.IO.Lazy as LazyIO

LazyIO.run . mapM (LazyIO.interleave . f)

Нет.