Haskell 中的评估顺序

Sequential ordering of evaluation in Haskell

为了在函数上下文中获得顺序评估,在 JavaScript 中,我经常使用

const right = a => b => b;

const f = () => right(console.log("hi"))(true);

console.log(f());

这有点类似于 do 线程,实现起来更简单,但只适用于热切求值语言,例如 JavaScript。

在 Haskell 中,由于语言以 lazy-evaluation/call-by-need 方式工作,因此不会计算 a 的参数,因为它在 lambda 表达式主体中不是必需的。

所以,我想知道实现在惰性求值中起作用的 right 函数的简单智能方法是什么。 <- 这个这是我的第一个问题。

有 Haskell 篇 wiki 文章:

Sequential ordering of evaluation

The seq primitive, despite its name, is not required to evaluate its parameters in some predefined order, according to the Haskell 2010 Report. For parallel programming, sometimes a variant of seq is needed with such an ordering:

While we focus mainly on the implementation, our work has had some impact on the programming model: we identify the need for pseq as well as seq (Section 2.1), and we isolated a signficant difficulty in the "strategies" approach to writing parallel programs (Section 7). Runtime Support for Multicore Haskell; Simon Marlow, Simon Peyton Jones and Satnam Singh. Instead of two or more similar primitives with similar purposes, extend the existing seq primitive with the traditional ordering of evaluation for its parameters - first, then second; with the role of simple strictness-control being performed by a new primitive with a more appropriate name e.g. amid, in Strictness without ordering, or confusion.

Strictness without ordering, or confusion

As the Haskell 2010 Report does not specify any order of evaluation with respect to its parameters, the name of the primitive seq :: a -> b -> b is a misnomer.

Introduce the primitive amid, with the same (Haskell 2010 Report) requirements:

     infixr 0 `amid`
     primtive amid :: a -> b -> b

     infixr 0 $!
     ($!) :: (a -> b) -> a -> b
     f $! x =  x `amid` f x

其实功能和我的完全一样right;但是,我不明白的是,正如我在惰性评估中提到的那样,由于函数体中不需要 a ,因此不应对其进行评估。那么这里发生了什么? #2 问题

我认为的代码是

right = \a -> \b -> (const a . const b) 0

但是,我不知道这个是否稳定。

一个主要区别是 Haskell 是纯的,而 JavaScript 不是。在 Haskell 中,如果从未使用过计算结果,则无论是否对其求值(忽略运行时)对程序的语义都没有影响。这是因为您不能在纯函数中产生副作用,包括将内容打印到控制台,因此如果您不使用该函数的结果,就好像您从未调用过它一样。
打印输出(即具有不纯函数)的唯一方法是它是否在 IO monad 中。所以像这样的函数:

f :: Int
f = const 5 (putStrLn "hello")
-- const is just your `right` function with its arguments reversed

不可能打印任何东西,因为函数是纯函数(如类型签名所示)。

即使你使用seq

f :: Int
f = let x = putStrLn "hello" in x `seq` const 5 x

你不会看到任何打印出来的东西——因为 seq 只会强制 x 到弱头范式,即只评估 IO 构造函数,但 evaluation of an IO action does not imply its execution

( 编辑:使 IO 效果真正发生的唯一方法是将其置于 main,这是一种特殊的 IO 动作。在上述 f 的定义中,表达式的计算结果为 5 :: Int 并且 putStrLn "hello" 对于任何外部函数都不存在,因此它永远不会成为 [ 的一部分=21=]。感谢@amalloy 的更正。)

唯一的方法是使用 >>= 函数。但是,>>= 的第二个参数需要 return 和 IO a。这意味着您只能强制发生副作用,然后 return 一个值,如果该值在 IO monad 中被 returned。

总而言之,monad 是保证您正在寻找的顺序评估的唯一方法:

f :: IO Int
f = do putStrLn "hello"
       return 5

或等价地,

f = putStrLn "hello" >> return 5

-- Can also be written as:
f = putStrLn "hello" >>= \_ -> return 5

Haskell 是懒惰和纯粹的事实有两个与您的问题相关的结果:

1。你不能写这个函数

语言中根本没有允许您说“评估这个,然后评估那个”的工具。正如您发现的 seq 最接近,因为它确保在 return 右元素之前评估其左参数。但它可能会评估 b,然后 a,然后 return b,如果它喜欢的话。

2。你不需要这个功能

计算 Haskell 中的表达式永远不会 1 有任何可观察到的副作用。所以 flip const,等同于 (\x y -> y),和你想象的这个函数有相同的行为。它 return 是它的第二个参数,你可以想象它会根据需要评估左边的参数,这没有效果,因为评估永远不会。您提议的 right 实现有问题,因为它 return 是 参数而不是右参数,但除此之外它再次具有相同的行为。

在确实需要顺序排序的情况下,它不是 evaluation 的排序,而是 IO 中嵌入的 effects 的排序。 Vikstapolis 的回答已经很好地涵盖了这个主题,因此我将不再赘述。


1 好吧,实际上这个规则有很多例外。但他们都以某种方式作弊,并没有真正考虑到这里。一些最常见的例外情况:

  1. 计算一个表达式可能会分配内存,或者导致某些 currently-pending 工作实际完成。如果您需要这些效果,那正是 seq 的用途。
  2. Debug.Trace 允许您将日志写入控制台,作为计算 otherwise-pure 表达式的一部分。这仅供调试使用,因此 fine-tuning 排序没有 standard-library 函数:如果您关心表达式生成的日志,那么您也应该真正关心表达式本身。 Debug.Trace 中的东西已经建立在 seq 之上,所以如果 left 所做的只是记录一些东西,你可以只使用 Debug.Trace 中的函数,例如 trace "hi" True.
  3. unsafePerformIO 可以在 supposedly-pure 表达式中执行任意 IO。它是为非常 low-level 的东西保留的工具,除非迫切需要,否则即使是语言专家也会避开它。没有初学者应该担心这个功能。