Purescript 中 Traversing/Binding/Fold-Binding 效果之间的差异

Differences between Traversing/Binding/Fold-Binding Effects in Purescript

我一直在努力解决这个问题,我已经编写了四个函数,我希望它们 运行 相同,我很好奇它们为什么不同。

toEffect :: Tuple Int String -> Effect Unit
toEffect (Tuple i strng) = 
  log $ append (show i <> ": ") $
  statefulPuzzleToString $ 
  selectFirstLadderBruteForce $
  parsePuzzle strng

main1 :: Effect Unit
main1 = (toEffect $ Tuple 1 $ fromMaybe "" $ hardestBoardStringsX11 !! 0) >>=
  (\_ -> toEffect $ Tuple 2 $ fromMaybe "" $ hardestBoardStringsX11 !! 1) >>=
  (\_ -> toEffect $ Tuple 3 $ fromMaybe "" $ hardestBoardStringsX11 !! 2) >>=
  (\_ -> toEffect $ Tuple 4 $ fromMaybe "" $ hardestBoardStringsX11 !! 3)
  -- ... Pattern could continue for all 11 boards
  
main2 :: Effect Unit
main2 = do
  toEffect $ Tuple 1 $ fromMaybe "" $ hardestBoardStringsX11 !! 0
  toEffect $ Tuple 2 $ fromMaybe "" $ hardestBoardStringsX11 !! 1
  toEffect $ Tuple 3 $ fromMaybe "" $ hardestBoardStringsX11 !! 2
  toEffect $ Tuple 4 $ fromMaybe "" $ hardestBoardStringsX11 !! 3
  -- ... Pattern could continue for all 11 boards

main3 :: Effect Unit
main3 = foldl 
  (\acc new -> acc >>= \_ -> new)
  (pure unit)
  effects
  where
    effects :: Array (Effect Unit)
    effects = map toEffect $ mapWithIndex Tuple hardestBoardStringsX11

main4 :: Effect Unit
main4 = traverse_ toEffect $ mapWithIndex Tuple hardestBoardStringsX11

对于前两个,控制台似乎会显示每个效果。日志语句之间可能有超过 1/2 秒的延迟。我会非常惊讶地看到它们的行为不同,因为我知道 main2 中的 do 符号只是 main1

中所写内容的语法糖

后两个出现同时记录他们的语句。

我不完全确定 main4,但我非常有信心 main3 真的应该和前两个一样。

对这里发生的事情有任何了解吗?

main3main4 出于同样的原因都会这样做,原因是 评估 执行之间的差异.


当你有一个 Effect a 类型的值时,它表示产生 a 的某种效果,大概你从某个地方得到了那个值。比方说:

myEffect = makeMeAnEffect "foo"

此值已在 makeMeAnEffect 求值 ,但尚未 执行 。在这里,“评估”意味着进行任何必要的计算以产生类型 Effect a 的值。创建此值可能涉及一些计算 - 例如数字相乘、字符串遍历、矩阵相加。这就是所有的“评价”。

但是评估的结果是对 执行 时应该发生什么的“描述”。这里“执行”的意思是“运行产生效果,使它描述的任何动作发生。

评估和执行在技术上是不同的概念。许多语言将它们混为一谈,但是纯函数式语言,例如 PureScript 和 Haskell,保持严格的分离:首先创建对应该发生的事情的描述(“评估”),然后是“运行”描述(“执行”)。

这种区别在实践中非常重要:“求值”是纯粹的,这意味着除了结果之外它是完全不可观察的,因此编译器可以用它做任何它想做的事——例如优化,roll/unroll,甚至完全删除,- 只要其结果保持不变。另一方面,“执行”必须按照程序员指定的确切方式执行,因为它的全部意义在于产生效果,所以弄乱它会产生明显的后果。


在您的特定情况下,在 toEffect 的正文中,评估是 log $ 之后发生的一切。所有对 appendselectFirstLadderBruteForce 的调用,等等,所有这些都是“评估”。 None 是有效的。您正在执行一些计算以弄清楚您将要创建什么样的效果。

然后,一旦你完成了所有的计算,你将它的结果传递给 log,这使你成为一个 Effect Unit,这是“应该发生什么的描述”。在这种特殊情况下,“应该发生什么”非常小——只需向控制台写入一个字符串。


现在,我们终于可以了解 main1/main2main3/main4 之间的区别了。

main1main2 中,您仅在执行第一个效果后才创建每个下一个效果。所以评估和执行“重叠”,可以这么说:首先你做拳头评估,创造拳头效果,然后你 运行 它,然后,只有在它完成后 运行ning,你才继续做二次评价,创造二次效应。等等。由于昂贵的部分(在您的情况下)是评估,因此每次下一次执行都会延迟下一次评估花费的时间。

另一方面,在 main3main4 中,您首先进行评估,通过在数组上调用 map toEffect 立即创建所有效果,然后才继续一个一个执行。同样,由于评估(在您的情况下)是昂贵的部分,并且所有这些都在开始时发生,因此执行不会延迟。每个效果都非常小(只是打印到控制台),所以它们都执行得非常快。


如果你真的想在上一次执行完成之前阻止下一次评估发生,你可以这样做:在 toEffect 的开头添加一个 pure unit 像这样:

toEffect (Tuple i strng) = do
  pure unit

  log $ append (show i <> ": ") $
  statefulPuzzleToString $ 
  selectFirstLadderBruteForce $
  parsePuzzle strng

这将确保在第一行 执行 之前,第二行不会开始 评估 ,从而使每个评估都发生仅在其各自执行之前。


最后,还有一个有趣的事实:在 Haskell 中,相同的程序会以不同的方式运行,因为 Haskell 是惰性的。当被要求进行评估时,它不会立即执行,而只是“记住”它被要求这样做。并且只有当评估的结果确实是必要的时候(这会在执行时发生),才会执行。

另一方面,PureScript 是严格的,这意味着它总是会立即计算所有内容。在这种特殊情况下,这意味着它将计算整个 append 等系列调用,然后才能将其结果传递给 log.