如何使用参与者系统对 if 表达式建模?

How to model if-expressions with actor systems?

我是 trying to emulate 一种简单的函数式语言,它使用基于角色的执行模型,其中出现了对 if 表达式建模的问题。

现在的Actor系统基本上都是speeding up all kind of stuff by avoiding OS locks and stalled threads or to make microservices less painful, but initially it was supposed to be an alternative model of computation in general [1][2], a contemporary take on it may be propagation networks用的。所以这应该能够涵盖任何编程语言结构,当然还有 if,对吧?

虽然我知道这偶尔会引起恼怒,但我看到 one timid attempt to move towards recursive algorithms represented using akka actors (I've refurbished it and added further examples 包括下面给出的那个)。该尝试在函数调用处停止,但为什么不更进一步,对操作符和 if 条件与参与者进行建模呢?事实上,smalltalk 语言应用了这个模型并产生了 actor 概念的前身,正如在下面接受的答案中所指出的那样。

令人惊讶的是,递归函数调用并不是什么大问题,但 if1 是,因为它具有潜在的状态性质。

给定子句 C: if a then x else y,问题如下:

我最初的想法是,C 是一个 actor,其行为类似于具有 3 个参数 (a,x,y) 的函数,return 是 xy取决于 a。最大并行 [2] a,xy 将被同时评估并作为消息传递给 C。现在,这不是很好,如果 C 是递归函数 f 的退出条件,则在无限递归中发送 f 的一个分支。此外,如果 xy 有副作用,则不能同时评估它们。让我们采用这个递归和(不是通常的阶乘,它本身很愚蠢,可以做成尾递归,但这不是重点)

f(n) =  if n <= 0
      0
    else
      n + f(n -1)

请注意,我想创建一个类似于 Scala 的 if 表达式,请参阅(spec,第 88 页)或 Haskell,就此而言,而不是依赖于副作用的 if 语句。

f(0) 会导致 3 个并发计算

我可以从这里看到这些选项:

我现在不确定,如果我没有错过基本层面的问题,还有其他明显的方法我只是看不到。输入赞赏:)

顺便说一句。请参阅 this for an exhaustive list of conditional branching in different languages, without giving their semantics, and, not exhaustive, the wiki page on conditionals, with semantics, and this 讨论手头的问题是如何一直到硬件级别的问题。

1 我知道 if 可以被视为模式匹配的特例,但问题是,如何对不同的情况进行建模使用演员的匹配表达式。但也许这根本不是故意的,匹配只是每个演员都可以做的事情,而无需参考其他专门的“匹配演员”。另一方面,有人说“万物皆演员”,颇为迷惑[2]。顺便提一句。有没有人清楚地知道 [#message whatever] 符号在那篇论文中意味着什么? # 是令人恼火的未定义。也许smalltak给出了一个提示,那里表示一个符号

Q : didn't ( I ) miss out on the question at a fundamental level?

是的,您碰巧错过了一个要点:即使是函数式语言,它们可能以 AND-and/or 的形式享受基于 OR 的细粒度并行性,但也不会像狂野到不尊重 [SERIAL] if ( expression1 ) expression2 [ else expression3 ]

的严格性质

您在递归案例的争论上花费了很多精力,而主要的 属性 却被您忽略了。状态完整是计算的母性(这些玩具不过是有限状态自动机,无论状态有多大-space,它基本上是并且永远是有限的)。

甚至引用的 Scala p.88 也证实了这一点:"The conditional expression is evaluated by evaluating first e1. If this evaluates to true, the result of evaluating e2 is returned, otherwise the result of evaluating e3 is returned." - 这是一个纯粹的 [SERIAL] 过程配方(一个接一个的步骤)。

人们可能还记得,即使是 expression1 的计算也可能有(并且确实有)状态变化效应(不仅是 "side-effects" ),但确实是状态变化效应(每当要求生成随机数和许多类似情况时,PRNG 都会进入新状态)

因此 if e1 then e2 else e3 必须服从纯 [SERIAL] 实现,无论从细粒度 {AND |OR}-based-parallelism(自 70 年代末 80 年代初以来,可能会在可以正确使用它们的语言中看到许多工作示例)

你的问题有点误解。在函数式语言中,if 不一定是三个参数的函数。相反,它有时是两个参数的两个函数。

特别是 Church 布尔值编码 在 λ 演算中的工作原理:有 两个 函数,我们称它们为TrueFalse。这两个函数都有两个参数。 True 只是 return 第一个参数,False 只是 return 第二个参数。

首先,让我们定义两个函数 truefalse。我们可以用任何我们想要的方式定义它们,它们是完全任意的,但我们将以一种非常特殊的方式定义它们,这种方式有一些优势,我们稍后会看到(我将使用 ECMAScript 作为 λ-演算的某种合理的近似,这可能是与 λ-演算本身相比,本网站的更多访问者可读):

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els;

tru 是一个有两个参数的函数,它简单地忽略了它的第二个参数,returns 第一个。 fls 也是一个有两个参数的函数,它简单地忽略它的第一个参数和 returns 第二个参数。

为什么我们要这样编码 trufls?嗯,这样一来,这两个函数不仅代表了truefalse这两个概念,不,同时还代表了"choice"的概念,也就是说,它们也是 if/then/else 表达式!我们评估 if 条件并将其作为参数传递给 then 块和 else 块。如果条件的计算结果为 tru,它将 return 的 then 块,如果它的计算结果为 fls,它将 return 的 else 块.这是一个例子:

tru(23, 42);
// => 23

这个returns 23,还有这个:

fls(23, 42);
// => 42

returns 42,如你所料。

有一个皱纹,但是:

tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch

这会打印 then branchelse branch!为什么?

嗯,它 returns 第一个参数的 return 值,但它 计算 两个参数,因为 ECMAScript 是严格的,并且总是在调用函数之前评估函数的所有参数。 IOW:它评估第一个参数 console.log("then branch"),它只是 returns undefined 并且具有将 then branch 打印到控制台的副作用, and 它评估第二个参数,它也是 returns undefined 并作为副作用打印到控制台。然后,它 return 是第一个 undefined

在发明这种编码的 λ 演算中,这不是问题:λ 演算是 纯的 ,这意味着它没有任何副作用;因此你永远不会注意到第二个参数也被评估了。另外,λ-演算是 lazy(或者至少,它通常在正常顺序下计算),意思是,它实际上并不计算不需要的参数。所以,IOW:在 λ 演算中,永远不会计算第二个参数,如果是,我们也不会注意到。

然而,

ECMAScript 是 strict,即它总是计算所有参数。好吧,实际上,并非总是如此:例如,if/then/else 仅在条件为 true 时才评估 then 分支,并且仅评估如果条件为 false,则 else 分支。我们想用 iff 复制这种行为。值得庆幸的是,尽管 ECMAScript 不是惰性的,但它有一种方法可以延迟一段代码的计算,几乎所有其他语言都是这样做的:将它包装在一个函数中,如果你从不调用该函数,代码将永远不会被处决。

所以,我们将两个块都包装在一个函数中,最后调用 returned:

的函数
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch

打印 then branch

fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch

打印 else branch.

我们可以这样实现传统的if/then/else

const iff = (cnd, thn, els) => cnd(thn, els);

iff(tru, 23, 42);
// => 23

iff(fls, 23, 42);
// => 42

同样,我们在调用iff函数时需要一些额外的函数包装和iff定义中的额外函数调用括号,原因同上:

const iff = (cnd, thn, els) => cnd(thn, els)();

iff(tru, () => console.log("then branch"), () => console.log("else branch"));
// then branch

iff(fls, () => console.log("then branch"), () => console.log("else branch"));
// else branch

现在我们有了这两个定义,我们可以实现or。首先,我们看一下 table 对于 or 的真值:如果第一个操作数为真,那么表达式的结果与第一个操作数相同。否则,表达式的结果是第二个操作数的结果。简而言之:如果第一个操作数是true,我们return第一个操作数,否则我们return第二个操作数:

const orr = (a, b) => iff(a, () => a, () => b);

让我们看看它是否有效:

orr(tru,tru);
// => tru(thn, _) {}

orr(tru,fls);
// => tru(thn, _) {}

orr(fls,tru);
// => tru(thn, _) {}

orr(fls,fls);
// => fls(_, els) {}

太棒了!然而,这个定义看起来有点难看。请记住,trufls 本身已经像一个条件一样,所以实际上不需要 iff,因此所有的函数包装:

const orr = (a, b) => a(a, b);

你有它:or(加上其他布尔运算符)只用几行函数定义和函数调用定义:

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els,
      orr = (a  , b  ) => a(a, b),
      nnd = (a  , b  ) => a(b, a),
      ntt = a          => a(fls, tru),
      xor = (a  , b  ) => a(ntt(b), b),
      iff = (cnd, thn, els) => cnd(thn, els)();

不幸的是,这个实现相当无用:ECMAScript 中没有 return trufls 的函数或运算符,它们都是 return truefalse,所以我们不能将它们与我们的函数一起使用。但我们能做的还有很多。例如,这是一个单链表的实现:

const cons = (hd, tl) => which => which(hd, tl),
      car  = l => l(tru),
      cdr  = l => l(fls);

你可能注意到了一些奇怪的事情:trufls扮演着双重角色,它们同时充当数据值truefalse,但是在同时,它们也充当条件表达式。它们是 databehavior,捆绑成一个……呃……"thing"……或者(我敢说)对象!这种识别数据和行为的想法是否让我们想起了什么?

确实,trufls对象。而且,如果您曾经使用过 Smalltalk、Self、Newspeak 或其他纯面向对象的语言,您会注意到它们以完全相同的方式实现布尔值:两个对象 truefalse 具有方法named if 将两个块(函数、lambda 等)作为参数并计算其中之一。

这里有一个在 Scala 中可能看起来像的例子:

sealed abstract trait Buul {
  def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): T
  def &&&(other: ⇒ Buul): Buul
  def |||(other: ⇒ Buul): Buul
  def ntt: Buul
}

case object Tru extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): U = thn
  override def &&&(other: ⇒ Buul) = other
  override def |||(other: ⇒ Buul): this.type = this
  override def ntt = Fls
}

case object Fls extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): V = els
  override def &&&(other: ⇒ Buul): this.type = this
  override def |||(other: ⇒ Buul) = other
  override def ntt = Tru
}

object BuulExtension {
  import scala.language.implicitConversions
  implicit def boolean2Buul(b: ⇒ Boolean) = if (b) Tru else Fls
}

import BuulExtension._

(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3

考虑到 OO 和 actor 之间的关系非常密切(实际上它们几乎是一回事),这在历史上并不令人惊讶(Alan Kay 基于 Carl Hewitt 的 PLANNER 的 Smalltalk;Carl Hewitt 基于 Alan Kay 的 Smalltalk 的 Actors) ,如果事实证明这是朝着解决您问题的正确方向迈出的一步,我不会感到惊讶。