我正在学习如何使用State monad,我注意到在执行顺序方面有些奇怪的行为。删除涉及使用实际状态的令人分心的位,请说我有以下代码:
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
)
)
在ghci中运行runState mainAction 1
会产生以下输出:
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
版本会产生相同的输出。根据这里发生的执行顺序,实际的逻辑是什么?
答案 0 :(得分:21)
Haskell是 lazy ,这意味着事情不会立即执行。只要需要结果,就会执行任务 - 但不久就会执行。如果不需要代码,有时根本不会执行代码。
如果你在一个纯函数中粘贴了一堆trace
个调用,你会发现这种懒惰正在发生。首先需要执行的第一件事就是首先看到的trace
来电。
当某事说“"计算是从左到右"这意味着结果与从左到右计算的结果相同。实际发生的事情可能会有很大不同。
事实上为什么在纯函数中进行I / O是一个坏主意。正如你所发现的那样,你会感到很奇怪"结果是因为执行顺序几乎可以产生正确的结果。
为什么这是个好主意?当语言没有强制执行特定的执行顺序时(例如传统的"从上到下"在命令式语言中看到的顺序),编译器可以自由地进行大量的优化,例如不完全执行一些代码,因为它的结果并不需要。
我建议你不过多考虑Haskell中的执行顺序。应该没有理由。把它留给编译器。而是考虑一下你想要的值。该函数是否给出正确的值?然后它可以工作,无论它执行的是什么顺序。
答案 1 :(得分:7)
我认为这可能是
forM
的一个怪癖,并尝试使用sequence
,它特别指出它从左到右依次运行其计算:[...]
你需要学会做出如下的,狡猾的区别:
forM
,sequence
和类似函数承诺的是,效果将从左到右排序。因此,例如,以下内容保证以与字符串中出现的顺序相同的顺序打印字符:
putStrLn :: String -> IO ()
putStrLn str = forM_ str putChar >> putChar '\n'
但这并不意味着表达式按照从左到右的顺序进行评估。程序必须评估足够的表达式以确定下一个操作是什么,但这通常不需要评估早期操作中涉及的每个表达式中的所有内容。
您的示例使用State
monad,它是纯代码的最低点,因此突出了订单问题。在这种情况下,forM
的遍历函数唯一承诺的是映射到列表元素的动作中的get
将看到put
s对左边元素的影响在列表中。