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

Posted

技术标签:

【中文标题】如何使用参与者系统对 if 表达式进行建模?【英文标题】:How to model if-expressions with actor systems? 【发布时间】:2020-02-07 12:36:47 【问题描述】:

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

现在的 Actor 系统基本上用于speeding up all kind of stuff,通过避免操作系统锁和停滞的线程或make microservices less painful,但最初它应该是一般的替代计算模型[1][2],当代的上面可能是propagation networks。所以这应该能够涵盖任何编程语言结构,当然还有if,对吧?

虽然我知道这偶尔会遇到麻烦,但我看到 one timid attempt 转向使用 akka actor 表示的递归算法(我对其进行了翻新并添加了 further examples,包括下面给出的那个)。这种尝试在函数调用中停止了,但为什么不更进一步,对操作符和 if 条件进行建模呢?事实上,smalltalk 语言应用了这个模型并产生了 actor 概念的前身,正如在下面接受的答案中所指出的那样。

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

鉴于子句C: if a then x else y,问题出在:

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

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

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

f(0) 会导致 3 个并发评估

n &lt;= 0(好的)

0(好的)

n + f(n -1)(糟糕,引入了对 f(n) 的调用实际上会返回(产生 0)但对其分支的评估会无限继续的奇怪行为)

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

整个计算变成有状态的,x 的评估 或 y 仅在计算 a 之后发生(如果 x 或 y 有副作用,则为强制性)。

引入了一些保护机制,不会呈现 xy 适用于调用f时超出一定范围的参数。他们可能会评估一些“不适用”的标记而不是一个值,因为它来自一个不相关的分支,所以无论如何都不会在 C 中使用。

在这一点上我不确定,如果我没有从根本上错过这个问题,而且还有其他明显的方法,我只是看不到。输入赞赏:)

顺便说一句。请参阅this 以获取不同语言中条件分支的详尽列表,但未给出其语义,以及wiki page on conditionals(带有语义)和this 以讨论手头的问题如何成为一个问题到硬件级别。

1 我知道if 可以被视为模式匹配的一种特殊情况,但问题是,如何使用actors 对匹配表达式的不同情况进行建模。但也许这甚至不是一开始就打算的,匹配只是每个演员都可以做的事情,而无需参考其他专业的“比赛演员”。另一方面,有人说“一切都是演员”,相当混乱[2]。顺便提一句。有没有人清楚地知道[#message whatever] 符号在该论文中的含义是什么? # 令人恼火地未定义。也许 smalltak 给出了提示,那里表示a symbol。

【问题讨论】:

您的阶乘实现是有状态的,因为递归调用没有位于尾部(因为一旦f(n-1) 返回,您仍然需要添加一些内容)。我认为这是一个基本问题,在 FP 或命令式实现中也是如此。使累积递归函数 TCO 感知的技术可能会有所帮助。 (将累加器作为额外参数) 我知道这可以是尾递归的,这与递归或总和无关,它与“if”的实现有关。它甚至不是阶乘,这是一个愚蠢的总和;) 你能在这里与 Prolog 的occurs_check 问题做一个平行吗?我认为 if 分支的并行计算是一个问题本身,特别是如果您希望您的 if 成为递归函数中的保护条件。同时执行两个分支并最终选择结果的目的是通过实际做更多的工作来提前完成。但是递归保护的目的实际上是防止代码搜索不存在的东西。 (你的第二个分支实际上是这样做的) Laurent,我知道我不能做并行计算(这是一种选择),它可能没有实用的目标,但问题的一部分是,面对Hewitt ea 对参与者系统的初步提议进一步鼓励了它不过,感谢发生检查提示。而对于守卫方法的投票,是隐含的,还是你没有? 老实说,我无法为您提供任何进一步的信息,我只是模糊地了解演员模型 :) 我在以前的 cmets 中想说的关键:有一个命令式if 中的基本问题:它用于两种不同的事情-实际上是分支和保护recusion。但是命令式编程通过在其语义中不执行两个分支而摆脱了它(不匹配分支的副作用不应出现在后状态中)我会投票支持有状态的解决方案f 出现在其中一个分支中,第二个出现在其他情况下。 【参考方案1】:

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

具体来说,这就是 Church Encoding of Booleans 在 λ 演算中的工作方式:有 两个 函数,我们称它们为 TrueFalse。这两个函数都有两个参数。 True 只返回第一个参数,False 只返回第二个参数。

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

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

tru 是一个有两个参数的函数,它简单地忽略第二个参数并返回第一个参数。 fls 也是一个有两个参数的函数,它只是忽略它的第一个参数并返回第二个参数。

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

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

这将返回23,并且:

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

返回 42,正如您所期望的那样。

但是有一个皱纹:

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

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

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

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

然而,

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

因此,我们将两个块包装在一个函数中,并在最后调用返回的函数:

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。首先,我们查看or 的真值表:如果第一个操作数为真,则表达式的结果与第一个操作数相同。否则,表达式的结果是第二个操作数的结果。简而言之:如果第一个操作数是true,则返回第一个操作数,否则返回第二个操作数:

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 中没有返回trufls 的函数或运算符,它们都返回truefalse,所以我们不能将它们与我们的函数一起使用.但我们仍然可以做很多事情。例如,这是一个单链表的实现:

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

你可能注意到了一些奇怪的东西:trufls 起到双重作用,它们既充当数据值 truefalse,但同时又充当条件表达式.它们是数据行为,捆绑成一个……嗯……“事物”……或者(我敢说)对象!这种识别数据和行为的想法是否让我们想起了什么?

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

以下是 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 和演员之间非常密切的关系(实际上它们几乎是一回事),这在历史上并不令人惊讶(艾伦凯基于卡尔休伊特的计划者的 Smalltalk;卡尔休伊特基于艾伦凯的 Smalltalk 的演员),我如果这被证明是朝着解决您的问题的正确方向迈出的一步,我们不会感到惊讶。

【讨论】:

我将此作为社区 wiki 发布,因为它只给出了潜在的答案方向,而不是完整的答案,甚至不一定是正确的方向。 @curiosa:您链接的论文中明确提到了 Smalltalk。我不确定我是否有任何其他方向的文本参考,但我记得 Alan Kay 在几次采访中明确引用了 PLANNER,特别是目标导向执行模型如何影响 Smalltalk 的消息驱动执行模型,这反过来是Actors 消息驱动执行模型的灵感来源。 我考虑用 Scala 编写第一部分,但是 a) 我已经在 ECMAScript 中使用它,并且 b) 类型注释会分散注意力。 反对调用“实现”(第一次评估两者,接下来根据(if-ed)控制产生其中一个-state ) 反映选择。这可能在某种实现中起作用,但这不是我们所知道的选择。选择“杀死海狸(拯救树)”或“杀死树”通常会导致树或海狸保持活力。不在上面“实现”论证的示例中,您将同时收到一个死海狸和一个死树,并带有事后建议,代码宁愿只杀死其中一个(验尸建议) 是的,但是杀戮是一种副作用,对吧?没有副作用的功能解释呢?【参考方案2】:

(我)没有从根本上错过这个问题吗?

是的,您碰巧漏掉了一个要点:即使是函数式语言,本来可以享受基于 AND 和/或 OR 的细粒度并行性形式,也不会像不这样做一样疯狂。尊重if ( expression1 ) expression2 [ else expression3 ]

的严格[SERIAL]性质

您在关于递归案例的论证上花费了很多精力,而主要属性却不在您的视野中。状态充满是计算的本质(这些玩具只不过是有限状态自动机,无论状态空间有多大,它基本上是并且永远将主要保持有限)。

即使是引用的 Scala p.88 也证实了这一点:“条件表达式是通过先计算 e1 来计算的。如果计算结果为 true,则计算结果是 e2 返回,否则返回计算 e3 的结果。" - 这是一个纯粹的-[SERIAL] process-recipe(一步一步)。

人们可能还记得,即使是对 expression1 的评估也可能(并且确实有)状态变化效应(不仅是“副作用”),而且确实是状态变化效应(每当要求生成随机数和许多类似情况时,PRNG 都会进入新状态)

因此,if e1 then e2 else e3 必须服从纯-[SERIAL] 实现,无论细粒度基于 AND|OR 的并行性(可能从 70 年代末 80 年代初就可以使用它们的语言中的许多工作示例)

【讨论】:

感谢您的努力,但不知道。你看,例如如果a &gt; b then a - b else b - a 的并行评估完全不是IMO 的问题。我也很欣赏从演员的角度为它带来光明的答案。澄清一下:我不想模仿 Scala 的执行模型,它是关于使用表达式而不是语句。 “其中的许多工作示例......”:串行实现?当然,但如果你能说出一个平行的名字,我会很高兴。您能否通过进一步的工作或参考来证实您“必须遵守纯串行实现......”的说法?好像不太明显。

以上是关于如何使用参与者系统对 if 表达式进行建模?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用实体查询框架对以下查询进行建模

如何使用 RAII 对套接字进行建模

用例建模Use Case Modeling

用例建模Use Case Modeling

UML建模-用例图篇

Linux文件系统的用例建模