Порядок исполнения в монадах

Я учился использовать государственную монаду, и я заметил некоторое нечетное поведение с точки зрения порядка исполнения. Удаляя отвлекающие биты, которые связаны с использованием фактического состояния, скажем, у меня есть следующий код:

import Control.Monad
import Control.Monad.State
import Debug.Trace

mainAction :: State Int ()
mainAction = do
    traceM "Starting the main action"
    forM [0..2] (\i -> do
        traceM $ "i is " ++ show i
        forM [0..2] (\j -> do
            traceM $ "j is " ++ show j
            someSubaction i j
        )
    )

Запуск runState mainAction 1 в ghci выводит следующий результат:

j is 2          
j is 1          
j is 0          
i is 2          
j is 2          
j is 1          
j is 0          
i is 1         
j is 2          
j is 1          
j is 0          
i is 0          
Outside for loop

который выглядит как обратный порядок выполнения того, что можно ожидать. Я подумал, что, возможно, это причуда forM и попробовал его с помощью sequence, в котором, в частности, говорится, что он последовательно выполняет его вычисление слева направо так:

mainAction :: State Int ()
mainAction = do
    traceM "Outside for loop"
    sequence $ map handleI [0..2]
    return ()
    where
    handleI i = do
        traceM $ "i is " ++ show i
        sequence $ map (handleJ i) [0..2]
    handleJ i j = do
        traceM $ "j is " ++ show j
        someSubaction i j

Однако версия sequence дает тот же результат. Какова фактическая логика с точки зрения порядка исполнения, который здесь происходит?

Ответ 1

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

Если вы вставляете кучу вызовов trace в чистую функцию, вы увидите эту лень. Первое, что необходимо, будет выполнено первым, так что вызов trace, который вы видите первым.

Когда что-то говорит, что "вычисление выполняется слева направо", это означает, что результат будет таким же, как если бы вычисление выполнялось слева направо. То, что на самом деле происходит под капотом, может сильно отличаться.

Это на самом деле, почему это плохая идея делать I/O внутри чистых функций. Как вы обнаружили, вы получаете "странные" результаты, потому что порядок выполнения может быть практически любым, что дает правильный результат.

Почему это хорошая идея? Когда язык не применяет конкретный порядок выполнения (например, традиционный "сверху вниз" порядок, видимый на императивных языках), компилятор может выполнять тонну оптимизаций, например, например, не выполнять какой-либо код вообще, потому что его результат не требуется.

Я бы порекомендовал вам не слишком много думать о порядке выполнения в Haskell. Не должно быть никаких оснований. Оставьте это до компилятора. Вместо этого подумайте о том, какие значения вы хотите. Соответствует ли функция правильному значению? Затем он работает, независимо от того, в каком порядке он выполняет вещи.

Ответ 2

Я подумал, что, возможно, это причуда forM и попробовал его с помощью sequence, в котором, в частности, говорится, что он последовательно выполняет его вычисление слева направо так: [...]

Вам нужно научиться делать следующее, сложное различие:

  • Порядок оценки
  • Порядок эффектов (a.k.a. "действия" )

Что forM, sequence и аналогичные функции обещают, что эффекты будут упорядочены слева направо. Так, например, гарантировано, что печатаются символы в том же порядке, что и в строке:

putStrLn :: String -> IO ()
putStrLn str = forM_ str putChar >> putChar '\n'

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

В вашем примере используется монада State, которая доходит до чистого кода, что подчеркивает проблемы порядка. Единственное, что функции обхода, такие как forM promises, в этом случае состоит в том, что get внутри действий, сопоставленных с элементами списка, увидит эффект put для элементов слева от них в списке.