Combinators: A Centennial View

观看livestreamed事件: Combinators:百年庆典

Combinators: A Centennial View

最终抽象符号

在图灵机之前,在lambda微积分之前——甚至在Gödel的定理之前——就有组合子。它们是我们现在所知的通用计算的第一个抽象例子——1920年12月7日首次被提出。在历史的另一个版本中,我们的整个计算基础设施可能都建立在它们之上。但事实上,一个世纪以来,它们在很大程度上仍是一种好奇,是抽象和晦涩的顶峰。

不难看出原因。在1920年的原始形式中,有两个基本的组合子,年代k,它遵循简单的替换规则(现在用模式非常清晰地表示沃尔夫拉姆语):

年代
& # 10005

s[间][y_] [z_] - > x [z] [y [z]]

k
& # 10005

k(间)[y_] - > x

这个想法是,任何符号结构都可以由某种组合产生年代的年代,k作为一个例子,考虑a[[一][c]].我们不说什么一个bc是;它们只是符号对象。但考虑到一个bc我们如何构建a[[一][c]]好吧,我们可以用年代k组合子。

考虑(显然是晦涩难懂的)对象

年代
& # 10005

(年代[k [s]] [s [k [k]] [s [k [s]] [k]]]] [s [k [s [s [k] [k]]]] [k]]

(有时不是写(年代(KS) (S (KK) (S (KS) K))) (S K (K (S (SKK))))).

现在把它当作一个函数,应用到a, b, c(年代[k [s]] [s [k [k]] [s [k [s]] [k]]]] [s [k [s [s [k] [k]]]] [k]][一][b] [c].然后观察当我们重复使用the时会发生什么年代k组合替换规则:

CloudGet
& # 10005


            

或者,稍微不那么模糊一点:

组合进化图
& # 10005


            

经过一系列步骤,我们得到a[[一][c]]!关键是不管我们想要什么样的符号建构,我们总是可以建立一些组合年代的年代,k这将最终为我们做到这一点,并最终计算通用.它们等价于图灵机,微积分和其他所有我们知道的通用系统。但它们是在这些系统之前被发现的。

顺便说一下,下面是获得上述结果的Wolfram语言方法(/ /。反复应用规则直到没有任何改变):

年代
& # 10005

s[s[k[s]][s[k[k]][s[k[s]][k]]][s[k[s[k[k][k]]][k]][a][b][c]/。{s[x_][y_][z_]>x[z][y[z]],k[x_][y]>x}

是的,在Wolfram语言中使用组合子非常容易和自然,这并不是偶然的——因为事实上组合子是Wolfram语言核心设计的深层祖先的一部分。

不过,对我来说,组合者还有另一种深刻的个人共鸣。它们是非常简单的计算系统的例子,结果(我们将在这里详细地看到)显示了我所花费的同样显著的行为复杂性这么多年来在计算机世界里研究

一个世纪以前,特别是在没有真正的计算机来做实验的时候,我所开发的思考计算世界的概念框架是不存在的。但我一直认为,在所有系统中,组合符可能是最早的大“侥幸”我所做的一切最终发现在计算的世界里。

组合计算

假设我们想用组合子来做一些计算。第一个问题是:我们应该如何表现“某物”?显而易见的答案是:使用由组合子构建的结构!

例如,我们想要表示整数。这里有一个(起初看起来很奇怪)这么做的方法.取s [k]并多次申请(年代[k [s]] [k]].然后我们将得到一系列组合表达式:

组合进化图
& # 10005


            

就其本身而言,这些表达是惰性的年代k规则。但拿每一个(说e)和形式e[s] [k].这是上面第三种情况的例子当你应用年代k规则:

组合进化图
& # 10005


            

要在Wolfram语言中得到它,我们可以使用,嵌套地应用函数:

巢
& # 10005

巢(f, x, 4)

则上述最终结果为:

巢
& # 10005

嵌套[s[k[s][k]],s[k],2][s][k]//。{s[x_][y_][z_]>x[z][y[z]],k[x_][y]>x}

下面是一个涉及嵌套7次的示例:

巢
& # 10005

Nest[s[s[k][k], s[k], 7][s][k] //。[x_][y_] -> x[z][y[z]], k[x_][y_] -> x}

这给了我们一种表示整数的方法(也许看起来晦涩难懂)n. 只是形式:

巢
& # 10005

巢[[年代[k [s]] [k]], [k], n]

这是一个组合子表示n,我们可以通过应用[s] [k].好的,给定两个这样表示的整数,我们如何将它们相加,这里有一个组合子!这就是:

s [k [s]] [s [k [s [k [s]]]] [s [k [k]]]]
& # 10005

s [k [s]] [s [k [s [k [s]]]] [s [k [k]]]]

如果我们称之为+,让我们来计算加上[1][2][s][k],其中1和2由组合子表示:

组合进化图
& # 10005


            

这需要一段时间,但结果是:1+2=3。

这是4 + 3,给出了结果(年代[s [s [s [s [s [k]]]]]]](即7),虽然经过了51步:

组合固定点列表
& # 10005


            

那么做乘法呢?这里有一个组合子其实很简单:

年代
& # 10005

s [k [s]] [k]

这是对3的计算×58步后2-giving 6:

组合固定点列表
& # 10005


            

这是一个combinator对权力

年代
& # 10005

s[k[s[s[k][k]]][k]

这是对3的计算2使用它(需要116步):

组合固定点列表
& # 10005


            

有人可能会认为这是一种疯狂的计算方法,但重要的是它是有效的,顺便说一句,它的基本思想是在1920年发明的。

虽然看起来很复杂,但它非常优雅。你所需要的是年代k.然后你可以从它们构建任何东西:函数,数据,等等。

到目前为止,我们用的是数字的一元表示法。但我们可以设置组合符来处理二进制数。或者,例如,我们可以建立组合子来做逻辑运算。

想象一下k代表真实,并且s [k]代表虚假(所以,像如果[p,x,y]k [x] [y]给了xs [k] [x] [y]给了y).那么最小的组合子只是

s [s] [k]
& # 10005

(年代[s]] [s] [s [k]]

我们可以通过计算真值表(FF, FT, TF, TT)来验证:

组合固定点列表
& # 10005


            

搜索给出16个可能的最小组合表达式二输入布尔函数

SKTruthTable
& # 10005


            

通过合并这些(或者仅仅是一个的副本与非门)可以制作组合符来计算任何可能的布尔函数。事实上,在一般情况下,人们可以——至少在原则上——通过将其“编译”成组合子来表示任何计算。

这里有一个更详细的例子,来自我的书一种新的科学.这是一个表示110规则元胞自动机进化中的一步的组合子

风格
& # 10005


            

在这本书中,我们展示了在进化过程中,反复使用这个组合子,努力计算三个步骤110规则

110规则

还有一段路要走,包括定点组合符等等。但基本上,自我们知道110规则是通用的,这表明组合子也是。

一百年后……

一个世纪过去了,我们该如何看待组合符呢?在某种意义上,它们仍然可能是我们所知道的最纯粹的表示计算的方法。但它们对我们人类来说也很难理解。

尽管如此,随着计算和计算范式的发展,并变得越来越熟悉,似乎在许多方面,我们越来越接近组合子的核心思想。事实上,Wolfram语言的基本符号结构以及我个人在过去40年中构建的许多东西,最终都可以被认为是由最先出现在combinators中的思想所深深影响的。

计算可能是有史以来最强大的统一智力概念。但是计算机和计算的实际工程发展趋向于把不同的方面分开。有数据。有数据类型。有代码。有功能。有变量。这就是控制流。是的,在计算机系统工程的传统方法中,把这些东西分开可能是很方便的。但其实不需要这样。 And combinators show us that actually there’s no need to have any of these distinctions: everything can be together, and can made of the same, dynamic “computational stuff”.

这是一个非常强大的想法。但在原始形态下,它也会让我们人类迷失方向。因为要理解事物,我们往往依赖于“固定锚”我们可以赋予它意义.在纯净的,不断变化的海洋年代k像我们上面看到的组合器,我们只是没有这些。

尽管如此,还是有一个妥协——在某种意义上,这正是使我有可能建立全面的计算机语言现在的Wolfram语言。重点是,如果我们要能够用计算来表示世界上的一切,我们需要组合类结构提供的那种统一性和灵活性。但我们不只是想要原始的、简单的组合符。实际上,我们需要预先定义许多类似组合器的构念,这些构念与我们在世界上所表示的东西有特定的含义。

在实践层面上,关键思想是表现一切都是一种象征性的表达,然后说求这些表达式的值包括对它们重复应用变换。是的,Wolfram语言中的符号表达式就像我们从组合器中得到的表达式一样——除了不只是包含在内年代的年代,k是的,它们涉及数千种不同的符号结构我们定义要表示的分子,或城市多项式.但关键是,与组合器一样,我们正在处理的东西在结构上总是纯符号对象的嵌套应用程序。

我们从组合符中学到的是“数据”和“代码”没有什么不同;它们都可以用符号表达式表示。两者都可以作为计算的原材料。我们还了解到“数据”并不需要维护任何特定的类型或结构;不仅它的内容,而且它作为符号表达式构建的方式可以是计算的动态输出。

有人可能会认为这样的事情只是原理上的深奥问题。但是我在构建Wolfram语言的过程中学到的是,它们实际上是自然的,并且至关重要的,因为它们有方便的方法来计算我们人类思考事物的方式,以及世界的方式。

从实用计算的早期开始,人们就有一种直觉,认为程序应该设置为指令序列,例如“拿一件东西,然后对它做这个,然后再做那个”等等。其结果将是一个“程序化”程序,如:

x=f
& # 10005

x=f[x];x=g[x];x=h[x];x

但正如组合子方法所暗示的那样,有一种概念上更简单的方法来写这个,在这种方法中,一个人只是连续地应用函数,来制作一个“函数式”程序:

h
& # 10005

h (g (f [x]])

(在Wolfram语言中,也可以这样写h@g@f@xx / / f / / g / h.)

考虑到一切都是符号表达的概念,一个人马上就会有函数来操作其他函数,比如

巢
& # 10005

巢(f, x, 6)

或:

ReverseApplied
& # 10005

反向应用[f][a,b]

这种“高阶函数”的概念是典型的组合信息,非常优雅和强大。随着时间的推移,我们逐渐了解如何用Wolfram语言理解和访问it的更多方面(想想:褶皱MapThreadSubsetMapFoldPair,……)。

好的,但还有一件事是组合符做的,这是它们最著名的:它们允许一个人设置东西,这样一个人就不需要定义变量或命名东西。在典型的编程中,人们可能会这样写:

与
& # 10005

[{x = 3}, 1 + x^2]

f
& # 10005

F [x_]:= 1 + x^2

函数
& # 10005

函数[x,1+x^2]

X |-> 1 + X ^2
& # 10005

X |-> 1 + X ^2

但在所有这些情况下,实际名称都无关紧要x是多少。的x只是一个占位符,代表某人在代码中“传递”的东西。

但是,为什么人们不能只是“做管道工作”,指定某物应该如何传递,而不显式地命名任何东西呢?某种意义上,嵌套的函数序列f (g [x]]做一个简单的例子;我们不会给结果取名字g[x];我们只是把它作为输入f在“单管”中。通过建立类似于函数[x, 1 + x ^ 2]我们正在构造一个没有名字的函数,但是我们仍然可以应用到一些事情上:

函数
& # 10005

函数[x, 1 + x^2][4]

Wolfram语言为我们提供了一种摆脱x在这里:

(1 + #^2) &
& # 10005

(1 + #^2) + [4]

在某种意义上(slot)在这里的作用就像自然语言中的代名词:不管我们要处理的是什么(我们不打算命名它),我们想要找到“1加上它的平方”。

好的,那么一般情况下呢?这就是组合子提供的方法。

考虑这样一个表达式:

f
& # 10005

f (g [x] [y]] [y]

想象一下这也是我们想要的问[x] [y]f (g [x] [y]] [y].有办法定义吗没有提到变量的名字?是的,这是怎么做的年代k组合子:

组合进化图
& # 10005

组合进化图[{SKCombinatorCompile[f[g[x][y][y],{x,y}},“状态显示”,“显示样式”->{s->Style[s,Black,FontWeight->“SemiBold”],k->Style[k,Black,FontWeight->“SemiBold”],g->Style[g,Gray],f->Style[f,Gray]}]

没有提到xy在这里;组合子结构只是定义——没有命名任何东西——如何“传入”一个提供的“参数”。让我们拭目以待吧:

组合进化图
& # 10005


            

是的,它看起来非常模糊。多年来,我一直在努力寻找一个有用的、人类可以理解的“包装”,我们可以将其构建到Wolfram语言中,但到目前为止,我失败了。

但这很有趣——也很鼓舞人心——甚至在原则上有一种方法可以避免所有命名变量。是的,在编写程序时使用命名变量通常不是问题,名称甚至可能传递有用的信息。但有各种各样的缠结,他们可以让一个。

当一个名称是全局的,并且给它赋值会产生影响时,这种情况尤其糟糕(可能在不知不觉中)一切一个人的。但是,即使保持名称的范围本地化,仍然会出现许多问题。

考虑为例:

函数
& # 10005

函数[x, y, x + y]]

它是两个嵌套的匿名函数(AKA lambdas)x“得到”一个,y“得到”b

函数
& # 10005

函数[x, y, 2 x + y] [a][b]

但是这个呢:

函数
& # 10005

函数[x, x, x + x]]

Wolfram语言方便地把东西涂成红色,表示正在发生不好的事情。我们的名字有冲突,我们不知道是哪一个x应该是指什么。

这是一个非常普遍的问题;甚至在自然语言中也会发生。如果我们写“Jane追Bob”。简跑快。“我们说的很清楚。但是“简追简。简跑快。已经糊涂了。在自然语言中,我们避免使用代词(基本上是类似于在Wolfram语言中)。因为英语中(传统的)性别设置“简追鲍勃”。她跑快。碰巧起作用。但是“猫追老鼠。”它跑得很快。“又没有。

但组合学家解决了所有这些问题,实际上是通过给出一个符号过程来描述引用的位置。是的,到目前为止,计算机可以很容易地做到这一点(至少如果它们处理符号表达式,比如Wolfram语言)。但是一个世纪的过去,甚至是我们的计算经验似乎都没有让我们人类更容易遵循它。

顺便说一句,值得一提的是组合机还有一个“著名”的特性——实际上在组合机之前就已经独立发明了——现在,相当历史地,通常被称为“curry”。在Wolfram语言中,很常见的一种情况是,函数自然接受多个参数。地理距离[a, b]+(a, b, c)(或a + b + c)这些都是例子。但为了尽可能地统一,组合子只是让所有的“函数”名义上只有一个参数。

为了建立“真正”有多个参数的东西,我们使用像这样的结构f [x] [y] [z].从标准数学的角度来看,这是非常奇怪的:人们期望“函数”只是“接受一个参数并返回一个结果”,以及“将一个空间映射到另一个空间”(说实数到复数)。

但是,如果一个人的思维“足够具有象征性”,那就好了。在Wolfram语言中,它具有基本的象征性特征(以及组合概念中的远祖),我们也可以这样定义

f
& # 10005

F [x_][y_]:= x + y

为:

f
& # 10005

F [x_, y_]:= x + y

早在1980年,尽管我认为我当时还不知道combinators,但我确实在我的电脑上试过对称多处理器系统这是一个Wolfram语言的前身拥有的想法f [x] [y]能够等价于f (x, y).但这有点像强迫每个动词都是不及物动词——在很多情况下,这是非常不自然的,而且很难理解。

野外的组合者:一些动物学

到目前为止,我们一直在讨论用来计算我们想要计算的特定事物的组合符。但如果我们只是“从野外”随机选择可能的组合子呢?他们会怎么做?

在过去,这似乎不是个值得问的问题。但我现在已经花了几十年的时间研究简单程序的抽象计算宇宙,并建立了一个完整的"新科学“围绕着我对它们行为的发现。有了这个概念框架,研究“野外”的组合子就变得非常有趣,看看它们是如何表现的。

让我们从头开始。最简单的年代k在组合子规则下不会保持不变的组合子表达式的大小必须为3。总共有16个这样的表达:

组合进化图
& # 10005


            

它们没有做任何有趣的事情:它们要么完全不变,要么,比如k [s] [s],他们立即给出一个单一的符号(这里年代).

但是更大的组合子表达式呢?可能的大小组合子表达式的总数n长得像

表格
& # 10005

表[2^n CatalanNumber[n - 1], {n, 10}]

或一般

2 ^ n CatalanNumber
& # 10005

2^n CatalanNumber[n - 1] == (2^n Binomial[2 n - 2, n - 1])/n

或渐近:

渐近的
& # 10005


            

在4码的时候,同样没有什么有趣的事情发生。在所有80种可能的表达式中,到达一个固定点需要的最长时间是3步,这发生在4种情况下:

组合进化图
& # 10005


            

在规模为5的情况下,到达一个固定点所需的最长时间是4步,在448个案例中有10个案例是这样的:

CloudGet
& # 10005


            

在第6页,“暂停时间”的分布稍微宽一些:

CloudGet
& # 10005


            

最长停机时间为7分钟,实现方式为:

CloudGet
& # 10005


            

同时,创建的最大表达式的大小为10(从某种意义上说,它们总共包含10个表达式年代的或k的):

组合进化图
& # 10005


            

最终尺寸的分布有点奇怪:

直方图
& # 10005


            

的大小n5、实际上存在一个没有最终状态大小的间隙n–生成1个。但在2688个表达中,大小为6的只有12个表达大小为5(约0.4%)。

好的,那么如果我们使用7号,会发生什么呢?现在有16896个可能的表达式。还有一些新的东西:两个永远不会稳定(年代sss (SS)), (SSS党卫军(SS)):

{年代
& # 10005

{[年代[s]] [s] [s] [s] [s], s [s] [s] [s [s]] [s] [s]}

在一个步骤之后,第一个会发展到第二个,但是接下来的几个步骤会发生什么(我们稍后会看到其他可视化的内容):

组合进化图
& # 10005


            

总尺寸(即LeafCount,或“数目”年代“s”)的增长:

LeafCount
& # 10005


            

对数图显示,初始瞬态后,尺寸大致呈指数增长:

s7lengths
& # 10005


            

从连续的比率中我们可以看到一些精细的结构:

s7lengths
& # 10005


            

这最终是在做什么?稍加努力,就可以发现这些大小的瞬态长度为-83,然后是长度为23 + 2的序列n,其中连续尺寸的第二个差异由下式给出:

加入
& # 10005

加入[38 {0,0,0,-17}2 ^ n +{0 1 0、-135、189},表(0,n), 38 {0 1 0, 0, 1, 1, 0, 0, 0, 4} 2 ^ n +{-13 0, 6、7、1,0,1,0,-27},表[0,n + 2], 228年{0 1 0,0,1,1}2 ^ n + 2{-17年-20 0 3日,14}]

最后的大小序列是通过连接这些块和计算得到的积累积累列表]]-给出一个渐近的大小,它的形式是.所以,是的,我们最终可以用这个小小的7号组合子“弄清楚发生了什么”(我们稍后会看到更多细节)。但它的复杂程度值得注意。

好的,让我们回过头来看看其他7码的表达式。暂停时间分布(忽略不暂停的两种情况)基本上呈指数下降,但显示出一些异常值:

allres = ResourceFunction
& # 10005


            

最大有限暂停时间为16步,由(年代[s [s]]] [s] [s] [s](S (SS))瑞士):

组合进化图
& # 10005


            

最终尺寸的分布是(通过我们刚才看到的最大停顿时间表达式,最大值为41):

allfix7
& # 10005


            

那么大小为8会发生什么呢?有109,824个可能的组合子表达式。很容易发现,除了76个,其余的都能在最多50步内到达固定的点(最长的幸存者是s [s] [s] [s [s [s]]] [k] [k]SSS (S (SS))乐),在44步后停止):

allres8t50
& # 10005


            

在这些情况下,最后的不动点大多很小;这是它们的大小分布:

allres8t50
& # 10005


            

下面是暂停时间和最终大小的比较:

allres8t50
& # 10005


            

规模的异常值是s[s][k][s[s[s][s][s]SSK(S(SS)S)S),经过27步演变成大小为80的固定表达式(同时达到大小为86的中间表达式):

ListStepPlot
& # 10005


            

在停步小于50步的组合子表达式中,中间表达式大小最大为275s [s] [s] [s [s [s] [k]]] [k]SSS (S(构造论))K)(最终演变为(年代[s [s] [k]]] [k]S(S(SSK))K)经过26个步骤后):

ListStepPlot
& # 10005


            

那么,大小为8的表达式在50步之后不会停止呢?总共有76个表达式,其中46个是不等价的(从某种意义上说,它们不会很快演化为集合中的其他表达式)。

下面是这46个表达是如何增长的(至少直到它们达到10,000):

在商场
& # 10005


            

其中一些最终停止了。事实上,s[s][s][s][s][s][k[k]]SSS (SS) S(乐))仅经过52步就停止了,最终结果是k [s [k] [k [s [k] [k]]]]K (SK (K (SKK)))),最大表达大小为433:

ListStepPlot
& # 10005


            

下一个最短的暂停时间是s [s] [s] [s [s [s]]] [k] [s]KS SSS (S (SS))),需要89步生成大小为65的表达式:

ListStepPlot
& # 10005


            

那么我们有s [s] [s] [s [s [s]]] [s] [k]SK SSS (S (SS))),它停止(给出尺寸为10s [k][[年代[s [s [s [s]]] [s]]] [k]]SK(S(S(S(SS))S))K),但必须经过325步:

ListStepPlot
& # 10005


            

还有一个更大的例子有待观察:(年代[s] [s]] [s] [s [s]] [k](SSS)年代(SS) K),展示了一个有趣的IntegerExponent-like“嵌套生长模式,但经过1958个步骤后最终停止,在此过程中达到了21720的最大中间表达大小:

ListStepPlot
& # 10005


            

那其他的表达方式呢?s [s] [s] [s [s]] [s] [s [k]]显示很正常增长的大小:

ListStepPlot
& # 10005


            

在其他情况下,没有明显的规律。但我们可以通过绘制连续步骤的大小差异来了解会发生什么:

在商场
& # 10005


            

这里有一些明显的规律性的例子。其中一些显示出线性递增的规律,这意味着总体上t2增长的大小:

pairGraphic
& # 10005


            

singleGraphic
& # 10005


            

其他显示正常差异的增长,导致t3/2增长的大小:

pairGraphic
& # 10005


            

pairGraphic
& # 10005


            

其他的则是纯指数增长:

pairLogGraphic
& # 10005


            

有相当一部分有规律但低于指数级的增长,很像尺寸为7的情况[s][s][s][s][s]SSS党卫军(SS)) ~增长:

pairLogGraphic
& # 10005


            

我们刚才看到的所有案例都只涉及年代.当我们允许的时候k还有,举个例子s [s] [s] [s [s [s] [s]]] [k]SSS (S (SSS)) K),显示出有规律的、本质上像“阶梯”一样的生长:

组合固定点列表
& # 10005


            

还有一个例子[s[s][s][s[s][s][k](SS)年代SK (SS)):

组合固定点列表
& # 10005


            

在小范围上,这似乎是有规律的,但在大范围上,通过取差异揭示的结构,它似乎不那么有规律(尽管它确实有一定的IntegerExponent——“看):

CloudGet
& # 10005


            

目前还不清楚在这种情况下会发生什么。行为的整体形式看起来有点类似于上面最终终止的例子。继续走5万步,下面是发生的事情:

CombinatorEvolve
& # 10005


            

事实上,大小差异的峰值会继续变得更高,并具有形式上的值6 (17×2n+ 1)并且出现在形式的位置上2 (9×2n+2+n- 18)

这是另一个例子:s [s] [s] [s [s]] [s] [k [s]]SSS (SS) S (KS))).这种情况下的整体增长——至少200步——看起来有些不规则:

CloudGet
& # 10005


            

而差异揭示了一种相当复杂的行为模式:

CloudGet
& # 10005


            

但在1000步之后,似乎可以看到一些规律:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

即使在2000步之后,规律也更加明显:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

有一个很长的瞬变过程,但在这之后,在大小差异中会出现系统性的峰值nth海拔16487+3320的山峰n发生在第14步n2+ 59n+ 284. (而且,是的,看到这些奇怪的数字突然出现是很奇怪的。)

如果我们看看大小为10的组合子表达式会发生什么?我们在小表情中看到了很多重复的行为。但一些新的事情似乎正在发生。

1000步后s [s] [k] [s [s] [k] [s [s]] [s]] [k]构造论(构造论(SS) S) K)当我们看到它的尺寸差异时,似乎在做一些相当复杂的事情:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

但事实证明,这只是暂时的,在1000步左右之后,系统进入了一种持续增长的模式,类似于我们之前看到的:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

另一个例子是s[s][k][s[s][k][s[s]][s]][s]构造论(构造论(SS) S)).在2000步之后,似乎有一些规律性,也有一些不规律性:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

基本上是这样的:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

s[s][s][s[s[s[k]]][s][s[k]]SSS(S(SK)))S(SK))是一个相当罕见的永远持续的“嵌套式”增长的例子(在一百万步之后,得到的大小是597,871,806):

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

最后一个例子是(年代[s]] [s] [s] [s] [s [s] [k [k]]](SS)瑞士(SS (KK))).以下是前1000个步骤的作用:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

它看起来有点复杂,但似乎增长缓慢。但在4750步左右,它突然上升,迅速达到51462码:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

继续前进,还有更多跳跃:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

10万步之后,有一个明确的跳跃模式,但它不是很规则:

ListStepPlot
& # 10005


            

那么会发生什么呢?大多数情况下,它似乎保持了几千或更多的规模。但是,在218703步之后,它下降到319号。所以,有人可能会想,也许它会“消亡”。继续往下走,在第34,339,093步,它变小到27,尽管在第36,536,622步,它的大小是105,723。

继续进行更长的时间,你会看到它的大小再次下降(这里显示在一个向下采样的日志图中):

下来=转置
& # 10005


            

但是,突然,砰的一声。在第137,356,329步,它停止了,到达大小为39的固定点。是的,一个小小的组合子表达式就像(年代[s]] [s] [s] [s] [s [s] [k [k]]](SS)瑞士(SS (KK))可以做到这一切。

如果你以前没见过这种复杂性是相当令人震惊的.但在探索计算世界这么长时间后,我已经习惯了它。现在我把每一个新案子都看作是我的计算等价原则

关于年代k组合子是通用的计算工具。这告诉我们,无论我们想做什么计算,总是可以“编写一个组合程序”——也就是说,创建一个组合表达式来完成它。从这里可以得出,就像停止的问题图灵机-组合子是否会停止的问题是普遍存在的不可判定的

但我们在这里看到的新情况是,很难弄清楚将会发生什么,不仅是“一般”的复杂表达式,用于进行特定的计算,而且对于简单的组合表达式,可能会“在野外找到”。但是计算等价原理告诉我们为什么会发生这种情况。

因为它说,即使是简单的程序——以及简单的组合符表达式——也能导致像任何事情一样复杂的计算。这意味着他们的行为可以计算不可约,因此,找出将要发生什么的唯一方法本质上就是运行每一步,看看会发生什么。如果想知道在无限长的时间内会发生什么,就必须进行无限次的计算才能知道。

有没有其他方法来表述我们关于组合子行为的问题?最终,我们可以使用任何计算通用系统来表示组合子的作用。但是有些公式可能与现有的概念联系得更紧密,比如数学上的。举个例子,我认为可以想象我们上面看到的组合子大小的序列可以用一种更直接的数值方法得到,也许是通过类似的方法嵌套递归函数(我发现了这个2003年的一个例子):

f
& # 10005

f[n_]:=3f[n-f[n-1]]

f
& # 10005

f [n_ /;N < 1 = 1

nrf
& # 10005


            

可视化组合子

研究组合子的一个问题是很难想象它们在做什么。这和我们的生活不一样细胞自动机在那里人们可以制作黑白细胞阵列,并很容易地使用我们的视觉系统来了解正在发生的事情。

组合进化图
& # 10005


            

在元胞自动机中,规则会作用于邻近的元素,所以发生的每件事都有局部性。但在这里,组合规则一次有效地移动整个块,所以很难直观地追踪发生了什么。

但是,在我们讨论这个问题之前,我们能把大量的括号和字母用类似的方式表示出来吗

CombinatorEvolveList
& # 10005


            

更容易阅读?例如,我们真的需要所有这些括号吗?例如,在Wolfram语言中,而不是写作

一个
& # 10005

a [[c [d [e]]]]

我们可以写成

a@b@c@d@e
& # 10005

a@b@c@d@e

从而避免支架。

但使用不能避免所有分组指示。例如,代表

一个
& # 10005

一个[b] [c] [d] [e]

我们必须这样写:

(((a@b)@c) @d)@e
& # 10005

(((a@b)@c) @d)@e

在上面的组合子表达式中,我们有24对括号。通过使用,我们可以减少到10个:

CombinatorPlot
& # 10005


            

我们不需要展示,所以我们可以把它变小:

CombinatorPlot
& # 10005


            

当一个世纪前首次引入组合符时,重点是“多参数类函数”表达式,例如[b] [c](如出现在规则年代k),而不是“类嵌套函数”表达式,例如a [[c]].不要把函数应用看成是"右联想a [[c]]可以在没有括号的情况下编写,如下所示a@b@c-人们反而认为函数应用是左联想-所以[b] [c]可以不用括号写。(令人困惑的是,人们经常使用作为此左关联函数应用程序的符号。)

事实证明f (g [x]]形式在实践中比f [g] [x], 30多年来,在Wolfram语言中,对于左关联函数应用程序的符号调用并不多。但为了庆祝combinator诞生一百周年,我们决定引入应用程序()表示左关联函数应用。

这意味着一个[b] [c] [d] [e]现在可以写成

FunctionToApplication
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);FunctionToApplication [[b] [c] [d] [e]]

没有圆括号。当然,现在a [[c [d [e]]]]需要括号:

FunctionToApplication
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);FunctionToApplication [[b [c [d [e]]]]]

在这种符号中年代k可以不加括号写成:

FunctionToApplication
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);FunctionToApplication[{年代(间)[y_] [z_] - > x [z] [y [z]], k(间)[y_] - > x}]

上面的组合子表达式变成

FunctionToApplication
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);FunctionToApplication [s [s [s [s]] [k [s [s [s]] [s]] [s]]] [k [s [s [s]] [s]] [s] [s [s [s]] [k [s [s [s]] [s]] [s]]]]]

或者没有函数应用程序字符

CombinatorPlot
& # 10005


            

现在包含13对括号。

不用说,如果你考虑所有可能的组合表达式,平均的左和右结合度在括号计数方面都是一样的:n组合子表达式,两者平均都需要对;需要治疗的病例数量k巴黎是

二项
& # 10005

二项式[n - 1, k - 1]/k

(修订)"加泰罗尼亚的三角形”)。(没有结合性,我们处理的是组合子表达式的标准表示,这总是需要的n- 1对括号)

顺便说一下,“右结合”括号对的数量就是匹配的组合子表达式的子部分的数量_ [_] [_],而对于左关联括号对,则是匹配的数字_[_[_]].(在不结合的情况下,括号的数量是匹配的数量_ (_).)

如果我们看看上面最小的非终止组合子表达式的演变过程中的括号/括号计数[s][s][s][s][s](或称为年代年代年代(年代s)年代年代)我们发现:

ListStepPlot
& # 10005


            

换句话说,在这种情况下,左结合律平均会导致62%的右结合律的括号。我们稍后会更详细地讨论这个问题,但对于增长的组合子表达式,几乎总是会出现这样的情况,即左结合性是“圆括号避免的胜者”。

但即使我们有了“最好的括号避免”,仍然很难从文本形式看出发生了什么:

组合进化图
& # 10005


            

那么干脆去掉括号怎么样?我们可以用所谓的波兰语(或Łukasiewicz)“前缀”符号-我们写f (x)作为外汇f (g [x]]作为fgx.在这种情况下,上面的组合子表达式变成:

CombinatorPlot
& # 10005


            

或者,像传统的HP计算器一样,我们可以使用反向波兰“后缀”表示法,其中f (x)外汇f (g [x]]fgx(和就像惠普):

CombinatorPlot
& # 10005


            

总数符号总是等于标准“非结合”函数形式中括号对的数量:

组合进化图
& # 10005


            

如果我们以更大的规模来看待这个问题,会怎么样?”细胞自动机的风格”,年代?这是一个不太有启发性的结果:

组合进化图
& # 10005


            

运行50个步骤,并修复宽高比,我们得到(对于波兰的情况):

组合进化图
& # 10005


            

我们也可以用括号表示来制作同样的图片。我们只要拿一根绳子[s][s][s][s][s]并将每个连续的字符渲染为一个带有某种颜色的单元格。(如果我们只有一个基本的组合符,那就特别容易年代-因为这样我们只需要开始和结束括号的颜色。)我们还可以通过括号表示来制作“细胞自动机风格”的图片,比如SSS党卫军(SS).同样,我们所做的就是将每个连续的字符渲染为一个带有某种颜色的单元格。

从本质上看,结果总是与上面的波兰情况相反。不过偶尔,它们至少会揭示一些“计算的内部”。这是最后的组合子表达式s [s] [s] [s [s [s]]] [k] [s]]从上面以右联想的形式呈现:

组合进化图
& # 10005


            

从某种意义上讲,这样的图片将所有的组合子表达式转换为序列。但组合子表达式实际上是分层结构,由嵌套的符号“函数”调用形成。一种表示层次结构的方法

年代
& # 10005

s[s][s][s][s][s]/.{s->Style[s,Black,fontwweight->“SemiBold”]}

通过嵌套框的层次结构:

MapAll
& # 10005

MapAll[# /。{现代(b_) - >框架(行[{a”、“b}]], a_Symbol - >陷害[一]}&、s [s] [s] [s [s]] [s] [s],头- >真]

我们可以通过表达式中的深度为每个盒子着色:

框架组合图
& # 10005


            

现在,为了表示这个表达式,我们需要做的就是用表示深度的颜色来表示基本的组合子。这样做的话,我们可以将上面终止的组合子的演化形象化为:

组合进化图
& # 10005


            

我们也可以用3D渲染(高度是表达式中的“深度”):

组合进化图
& # 10005


            

为了测试这样的可视化,让我们(如上面所示)查看所有大小为8的组合子表达式,这些组合子表达式具有不同的演化,不会在50步内终止。以下是每种情况下的“深度图”:

在商场
& # 10005


            

在这些图片中,我们为每个元素绘制一个单元格stringified版本,然后按深度上色。但是对于特定的组合子表达式,可以考虑其他方法来表示每个元素的深度。这里有一些可能性,显示为进化的第8步[s][s][s][s][s]SSS党卫军(SS))(请注意,第一个级别实际上是“缩进级别”,如果每个级别都使用年代k被“漂亮地打印”在单独一行):

SKStep
& # 10005


            

MatchedBracketsPlot
& # 10005


            

这是一系列步骤的结果:

MatchedBracketsPlot
& # 10005


            

但从某种意义上说,这是对组合子表达式更直接的可视化是树,例如在:

组合进化图
& # 10005


            

组合进化图
& # 10005


            

注意,这些树可以通过将它们视为左或右“关联”来进行某种程度的简化,本质上是将左或右叶子拉入“分支节点”。

但是使用原始的树,我们可以问,例如,什么样的树的表达式产生的进化[s][s][s][s][s]SS党卫军(SS))。以下是前15个步骤的结果:

CombinatorExpressionGraph
& # 10005


            

在一个不同的渲染,这些成为:

树桩
& # 10005


            

这些是组合子表达式在连续步骤中的表示。但是每一步的规则都应用在哪里呢?我们会在下一节,按照我们目前的做法,我们总是在每一步只做一次更新。下面是一个更新发生在特定情况下的例子:

CloudGet
& # 10005


            

继续我们得到的更长(注意,在这个显示中有些行已经换行了):

组合进化图
& # 10005


            

我们编写组合子表达式的方式的一个特点是,任何组合子规则的“输入”总是对应于我们显示的表达式中的连续span。因此,当我们显示组合子表达式在进化的每一步的总大小时,我们可以显示哪一部分被重写了:

组合进化图
& # 10005


            

请注意,如预期的那样年代规则倾向于增加大小,而K规则减少它。

下面是我们上面展示的所有例子的规则应用程序分布:

在商场
& # 10005


            

我们可以通过包含深度来组合多种形式的可视化:

组合进化图
& # 10005


            

组合进化图
& # 10005


            

我们也可以在3D中做同样的事情:

组合进化图
& # 10005


            

下面的树呢?这是年代K以树为单位的组合规则:

CombinatorExpressionGraph
& # 10005


            

以下是该技术发展的前几个步骤的更新s [s] [s] [s [s [s]]] [k] [s]KS SSS (S (SS))

CombinatorExpressionGraph
& # 10005


            

在这些图片中,我们有效地在每一步突出“第一”子树匹配s [_] [_] [_]k [_] [_].为了获得整个进化的感觉,我们也可以简单地计算具有给定一般结构的子树的数量(比如_ [_] [_]_[_[_]]),在给定的步骤(请参见下面的也):

组合固定点列表
& # 10005


            

关于组合子行为的另一个指示来自于树的深度。除了总的深度(即Wolfram语言深度),还可以查看更新事件发生的深度(这里显示了下面的总大小):

组合进化图
& # 10005


            

以下是上述规则的深度配置文件:

在商场
& # 10005


            

不足为奇的是,随着增长的继续,总深度往往会增加。但是值得注意的是,除了在接近终止时,(至少在我们当前的更新方案中)更新似乎倾向于对表达式树的“高级”(即低深度)部分进行更新。

当我们写出像size-33这样的组合子表达式时

组合进化图
& # 10005


            

或者把它画成树

CombinatorExpressionGraph
& # 10005


            

从某种意义上说,我们是非常浪费的,因为我们重复了很多次相同的子表达式。事实上,在这个特定的表达式中,总共有65个子表达式,但只有11个不同的子表达式。

那么我们如何表示一个组合子表达式尽可能多地利用这些子表达式的共性呢?我们可以使用一个有向无环图(DAG)来代替树来表示组合子表达式,在DAG中,我们从表示整个表达式的一个节点开始,然后展示它如何分解为共享子表达式,每个共享子表达式由一个节点表示。

为了了解它是如何工作的,让我们首先考虑一个简单的例子f (x).我们可以把它表示成一个树,其中根代表整个表达式f (x),与头部只有一个连接f,和另一个论点x

ToDAG
& # 10005


            

表达式f (g [x]]仍然是一棵树:

ToDAG
& # 10005


            

但在f (f [x]]有一个“共享子表达式”(在本例中是f),并且图形不再是树:

ToDAG
& # 10005


            

f [x] [f [x] [f [x]]]f (x)是共享子表达式:

ToDAG
& # 10005


            

s [s] [s] [s [s]] [s] [s]]事情变得有点复杂:

ToDAG
& # 10005


            

对于上面的size-33表达式,DAG表示为

ToDAG
& # 10005


            

其中节点对应于出现在根的整个表达式的11个不同的子表达式。

那么从dag的角度来看,组合子进化是什么样的呢?以下是进化的前15个步骤[s][s][s][s][s]

ToDAG
& # 10005


            

下面是后面的一些步骤:

ParallelTable
& # 10005


            

在某种意义上,共享所有公共子表达式是指定组合子表达式的最大简化方式。甚至当表达式的总大小大致呈指数增长时,不同子表达式的数量可能仅呈线性增长——这里大致为1.24t

CombinatorEvolveList
& # 10005


            

观察连续的差异可以得出一个相当简单的模式:

ListStepPlot
& # 10005


            

下面是上述46个“增长大小-7”组合子表达式演变50步的DAG结果:

在商场
& # 10005


            

值得注意的是,其中一些具有相当的复杂性,而另一些具有相当简单的结构。

更新方案和多路系统

到目前为止,我们讨论过的组合者的世界可能看起来很复杂。但到目前为止,我们一直在做一个大的简化。这与组合规则的应用有关。

考虑组合表达式:

年代
& # 10005

(年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]]

有6个地方(有些重叠)s [_] [_] [_]k [_] [_]匹配这个表达式的某个子部分:

组合进化图
& # 10005


            

我们可以在表达式的树形式中看到同样的事情(匹配在它们的子树的根中表示):

CombinatorExpressionGraph
& # 10005


            

但现在的问题是:如果要应用组合子规则,应该使用哪一个?

到目前为止我们所做的是遵循一个特定的策略通常被称为最左侧outermost-which可以被认为是看combinator表达我们通常与括号等写出来并应用第一场比赛我们相遇在一个从左到右扫描,或在这种情况下:

组合进化图
& # 10005


            

在Wolfram语言中,我们可以使用以下方法找到匹配的位置:

expr =年代
& # 10005

expr =[年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]]

pos =位置
& # 10005

pos = Position[expr, s[_][_][_]

如上所示,这些匹配在表达式中:

expr =年代
& # 10005

expr =[年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]];

pos =位置
& # 10005

pos = Position[expr, s[_][_][_] | k[_][_]];

马帕特
马帕特
& # 10005

MapAt [expr陷害,pos]

这是火柴,按顺序排列位置

组合进化图
& # 10005


            

这里最左最外的匹配是位置为{0}的那个。

一般来说,指定子表达式位置的索引序列决定了在表达式树的每一层中,要到达子表达式,应该向左还是向右。索引0表示去“头”,即ff (x),或者是f [a] [b]f [a] [b] [c];索引1表示“第一个参数”,即xf (x),或者是cf [a] [b] [c].索引列表的长度给出了相应子表达式的深度。

我们会在下一节关于如何用索引定义最左最外以及其他方案。但这里需要注意的是,在我们的例子中位置没有给我们{0}部分;相反,它给出了{0,0,0,1,1,1}:

组合进化图
& # 10005


            

现在发生的是位置正在对表达式树进行深度优先遍历以查找匹配项,因此它首先沿着左侧树分支向下搜索,因为它在那里找到了匹配项,所以返回的结果就是这个。在我们将在下一节讨论的分类法中,这对应于最左边最里面的模式,尽管在这里我们将其称为“深度优先”。

现在考虑一个例子s [s] [s] [k [s] [s]].首先是我们目前使用的最左最外的策略,其次是新的策略:

组合进化图
& # 10005


            

有两件重要的事情需要注意。首先,在这两种情况下,最终结果是相同的。第二,在这两种情况下,所采取的步骤——以及达到最终结果所需的总数量——是不同的。

让我们考虑一个更大的例子:s [s] [s] [s [s [s]]] [k] [s]]KS SSS (S (SS))).在上面的标准策略中,我们看到这个表达式的进化在89步之后终止,表达式的大小为65。在深度优先策略下,进化仍然以相同的65大小的表达式结束,但现在只需要29步:

组合固定点列表
& # 10005


            

组合子表达式进化的一个重要特征是,当它终止时——无论使用何种策略——结果都必须是相同的。(这个“汇合”属性——我们将在后面详细讨论——与概念密切相关因果不变性在我们的物理模型中。)

如果进化没有终止会发生什么?让我们考虑上面发现的最简单的非终止情况:[s][s][s][s][s]SSS党卫军(SS)).以下是我们讨论过的两种策略如何增加规模:

CombinatorEvolveList
& # 10005


            

如果我们在连续的步骤上绘制大小的比率,差异就更明显了:

CombinatorEvolveList
& # 10005


            

在这两幅图中,我们可以看到,这两种策略一开始产生了相同的结果,但很快就出现了分歧。

我们看了两种特定的策略来选择要做的更新。但是有没有一种通用的方法来探索所有的可能性呢?事实证明,确实有——而且它是可以使用的多路系统,这正是在我们的物理项目

这个想法是做一个多路图其中有一条边表示可以从每个可能的“状态”(即组合表达式)执行的每个可能的更新。下面是这个示例的外观s [s] [s] [k [s] [s]]SSS(KSS))以上:

skrules={s
& # 10005


            

如果我们包含所有的“更新事件”,我们会得到以下结果:

skrules
& # 10005


            

现在,每个可能的更新事件序列都对应于多路图中的一条路径。我们上面使用的两种特定策略对应于以下路径:

skrules
& # 10005


            

我们看到,即使在这里的第一步,有两种可能的方法。但是除了分支之外,还有合并,而且无论选择哪个分支,都不可避免地会以相同的最终状态结束——实际上是应用组合子规则的唯一“结果”。

这里有一个稍微复杂一点的情况,一开始是一条唯一的路径,但经过4个步骤后,会有一个分支,但再经过几个步骤,所有的东西都会再次收敛到一个唯一的最终结果:

skrules
& # 10005


            

对于大小为4的组合表达式,在多路图中从来没有任何分支。在大小为5时,出现的多路图为:

skrules
& # 10005


            

在大小为6时,2688个可能的组合子表达式会产生以下多路图,上面所示的图基本上是最复杂的:

skrules
& # 10005


            

在尺寸为7时,更多的情况开始发生。有相当规则的结构,如:

skrules
& # 10005


            

以及以下情况:

skrules
& # 10005


            

这可以通过给出每个中间表达式的大小来总结,这里显示了由标准的从左到外的更新策略定义的路径:

MWCombinatorGraph
& # 10005


            

相比之下,以下是上述深度优先策略定义的路径:

MWCombinatorGraph
& # 10005


            

s[s][s][s[s[k]][k]SSS (S (SK)) K)在这种情况下,最左最外求值避免较长的路径和较大的中间表达式

MWCombinatorGraph
& # 10005


            

而深度优先评估需要更多步骤:

MWCombinatorGraph
& # 10005


            

(年代[s]] [s] [s [s]] [s](SS)年代(SS))给出一个更大但更统一的多路图(年代[s [s]]] [s] [s] [s]发展直接(年代[s]] [s] [s [s]] [s]):

MWCombinatorGraph
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[MWCombinatorGraph[s[s[s]][s][s], 15, " left - tmost outermost "], AspectRatio -> 1.2, ImageSize -> 480]

深度优先评估提供了一个略短的路径:

MWCombinatorGraph
& # 10005


            

在size-7表达式中,最大的有限多路图(94个节点)为[s[s[s]][s][s][k](年代(SS))构造论):

MWCombinatorGraphMinimal
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[MWCombinatorGraphMinimal[s[s[s]][s][s][k], 18], AspectRatio -> 1.2]

根据路径的不同,这可能需要10到18个步骤才能到达最终状态:

MWCombinatorGraphMinimal
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);直方图(长度/ @ FindPath [MWCombinatorGraphMinimal [s [s [s [s]]] [s] [s] [k], 18],“s [s [s [s]]] [s] [s] [k]”,“s [k [s [k] [s [s [s]] [k]]]] [k [s [k] [s [s [s]] [k]]]]”,无穷,所有],ChartStyle - > PlotStyles美元(“直方图”、“ChartStyle”),框架- >真的,FrameTicks - >{真,假}]

我们的标准最左边最外面的策略需要12个步骤;深度优先需要13个步骤:

MWCombinatorGraphMinimal
& # 10005


            

但在大小为7的组合子表达式中,基本上有两个不会导致有限多路系统:(年代[s]] [s] [s] [s] [k]年代SSSK (SS))(它立即演变为s [s] [s] [s [s]] [s] [k]),s[s[s][s][s][s][s]年代sss (SS))(它立即演变为[s][s][s][s][s]).

让我们考虑一下(年代[s]] [s] [s] [s] [k]. 对于8个步骤,有一个独特的进化路径。但在第9步,进化分支

skrules
& # 10005


            

由于存在两个不同的可能更新事件:

组合进化图
& # 10005


            

继续14个步骤,我们得到了一个相当复杂的多路系统:

MWCombinatorGraphMinimal
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[MWCombinatorGraphMinimal[s[s]][s][s][k], 14], AspectRatio -> 1.2]

但这还没有“完成”;红色圆圈中的节点对应于非固定点的表达式,并将进一步演化。那么,特定的评估顺序会发生什么呢?

以下是我们两个更新方案的结果:

MWCombinatorGraphMinimal
& # 10005


            

这里可以看到一些重要的东西:最左边的最外面的路径(分12步)指向一个定点节点,而深度优先的路径指向一个将进一步演化的节点。换句话说,至少在这个多路图中,我们可以看到,最左边的最外面的求值终止,而深度优先不终止。

只有一个可见的固定点(s [k]),但有许多“未完成的路径”。这些会发生什么?让我们看看深度优先评估。尽管它在14步之后没有终止,但它在29步之后终止了——生成相同的最终结果s [k]

组合进化图
& # 10005


            

事实上,这是一个普遍的结果(从20世纪40年代就知道了),如果一个组合子的进化路径将要终止,它必须在唯一的固定点终止,但也有可能路径根本不会终止。

以下是17步后的结果。我们看到越来越多的路径通向固定点,但我们也看到越来越多的“未完成路径”正在生成:

MWCombinatorGraphMinimal
& # 10005


            

现在让我们回到我们上面提到的另一个例子:s[s[s][s][s][s][s]年代sss (SS)).对于12个步骤,演变是独一无二的:

组合进化图
& # 10005


            

但在这一步,有两个可能的更新事件:

组合进化图
& # 10005


            

从那时起,在多路图表中出现了快速增长:

MWCombinatorGraphMinimal
& # 10005

CloudGet[”https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl"]; 图[s[s][s][s][s][s],18],AspectRatio->1.2]

这里重要的是没有固定点:没有可能的评估策略可以导向固定点。我们在这里看到的是一个一般结果的例子如果在组合子进化中有一个固定点,那么最左最外的求值总会找到它。

从某种意义上说,最左最外的评价是“最保守”的评价策略,以“失控进化”告终的倾向最小。如果我们比较一下它的增长和深度优先评估,就会发现它的“保守主义”:

组合进化图
& # 10005


            

看看多路图——以及其他图——一个显著的特征是“长脖子”的存在:对于许多步骤,每个计算策略都会导致相同的表达式序列,并且在每个步骤中只有一个可能的匹配。

但这种情况能持续多久?8码及以下的衣服总是有限制的(7码的最长“脖子”是用来穿的s[s[s][s][s][s][s]它的长度是13;对于8号,它不再是,但是对于8号,它的长度是13[s[s[s][s][s][s][s]]k[s[s][s][s][s][s]]).但在规模为9的情况下,有四种(3种不同的)增长永远持续,但总是独特的:

{[年代[s [s]]] [s [s [s] [s]]] [s], s [s [s [s]]] [s [s [s]]] [[s]], < br / > s [s [s]] [s] [s [s [s] [s]] [s]], s [s [s]] [k] [s [s [s] [s]] [s]]}
& # 10005

{[年代[s [s]]] [s [s [s] [s]]] [s], s [s [s [s]]] [s [s [s]]] [[s]], s [s [s]] [s] [s [s [s] [s]] [s]], s [s [s]] [k] [s [s [s] [s]] [s]]}

正如人们所料,所有这些都显示出相当规律的增长模式:

组合进化图
& # 10005


            

在第一种和第三种情况下,第二种差异是通过连续的重复来表示的n):

加入
& # 10005

Join[{0,0,1},表[0,n],{7,0,0,0,1,0,3(2^(n+2)-3)}]

在第二种情况下,它们是由重复的

加入
& # 10005

加入[表[0 n] {2}]

在最后一种情况下,通过重复

加入
& # 10005

加入({0,1}、表(0,n), {3 2 ^ (n + 3) + 18日3 2 ^ (n + 3) - 11, 0, 1, 0, 3 2 ^ (n + 3) + 2, 9 2 ^ (n + 2) - 11}]

评估令问题

作为一名计算语言设计师,这是我追求了40年的一个问题:定义计算(即计算)顺序的最佳方法是什么?好消息是,在设计良好的语言中(如沃尔夫拉姆语!)基本上不重要,至少大多数时候是这样。但在考虑组合者及其进化方式时,评估顺序突然成为一个中心问题。事实上,这也是我们新物理模型的核心问题,它对应于参照系选择,学习相对论、量子力学等。

让我们首先讨论在Wolfram语言的符号结构中显示的求值顺序。假设你在做这样的计算:

长度
& # 10005

长度[Join[{a, b}, {c, d, e}]]

结果并不令人惊讶。但这里到底发生了什么?首先是计算加入[...]

加入
& # 10005

连接[{a, b}, {c, d, e}]

然后你得到结果,并将其作为论点提供给长度,然后执行它的工作,并给出结果5。而在通用的Wolfram语言,如果你在计算f (g [x]]接下来会发生什么呢x将首先评估,然后是g[x],最后f (g [x]].(实际上,ff (x)第一件事是评估的吗f (x, y)评估f,然后x,然后y然后f (x, y).)

通常这正是一个人想要的,也是人们隐含的期望。但也有一些情况并非如此。例如,假设您已经定义了x= 1(即。[x,1]).现在你想说x= 2 ([x,2]).如果x首先评估,你会得到[1,2],这没有任何意义。相反,你想要的保留它的第一个参数和“消费它”而不先评估它。在Wolfram语言中,这是自动发生的,因为有属性HoldFirst

这与组合器有什么关系?好吧,基本上,Wolfram语言使用的标准求值顺序类似于我们上面描述的深度优先(最左侧最内侧)方案,而当函数持有属性类似于最左最外的方案。

但是,好吧,如果我们有f [[x], y]我们通常首先评估[x],然后使用结果进行计算f [[x], y].这很容易理解[x],例如,立即求出类似4的值,而它本身不需要求值。但是当f [[x], y][x]计算结果为b (x)结果就是c (x)等等在“返回”评估之前,您是否完成了完整的“子评估”链y,f[…]

这与组合子有什么相似之处呢?基本上就是当你基于组合子表达式中的特定匹配进行更新时,你是否只是继续“更新更新”,或者你是否继续在对更新结果做任何事情之前,在表达式中找到下一个匹配。“更新更新”方案基本上就是我们所说的深度优先方案,它本质上是Wolfram语言在它的自动评估过程中所做的。

假设我们给出Wolfram Language赋值的组合子规则:

s[x_u][y_u][z]:=x[z][y[z]]
& # 10005

s[x_u][y_u][z]:=x[z][y[z]]

k(间)[y_]: = x
& # 10005

k(间)[y_]: = x

然后,根据Wolfram语言的标准评估过程,每次我们输入组合子表达式时,这些规则将自动重复应用,直到达到一个固定点:

s [s] [s] [s [s [s]]] [k] [s]
& # 10005

s [s] [s] [s [s [s]]] [k] [s]

“里面”到底发生了什么?如果我们在一个更简单的情况下跟踪它,我们可以看到有重复的计算,使用深度优先(又名从左到内)的方案来决定要计算什么:

数据集
& # 10005

数据集[跟踪[s [k [k] [k]] [s] [s]]]

当然,鉴于上面的任务年代,如果输入类似组合子表达式s [s] [s] [s [s]] [s] [s]]-其评估不会终止,会有麻烦,就像我们定义一样xx+ 1(或x= {x}),并要求x.当我第一次做语言设计时,人们经常告诉我,像这样的问题意味着使用自动无限计算的语言“根本无法工作”。但40多年后,我想我可以自信地说,“无限计算的编程,假设不动点”在实践中非常有效——在很少的情况下,如果没有不动点,人们无论如何都必须做一些更谨慎的事情。

在Wolfram语言中,这都是关于具体应用规则,而不是让它自动发生年代k

清晰的
& # 10005

清除[s,k]

现在没有相关的变换年代k将自动生成:

年代
& # 10005

s [s] [s] [s [s [s]]] [k] [s]

但通过使用/.ReplaceAll),我们可以问年代k转换规则应用一次:

年代
& # 10005

s [s] [s] [s [s [s]]] [k] [s] /。[x_][y_] -> x[z][y[z]], k[x_][y_] -> x}

FixedPointList我们可以继续应用这个规则,直到我们到达一个固定点:

FixedPointList
& # 10005

FixedPointList[# /。{年代(间)[y_] [z_] - > x [z] [y [z]], k(间)[y_] - > x} &、s [s] [s] [s [s [s]]] [k] [s]]

它需要26步——这与最左最外的89步,或最左最内(深度优先)的29步不同。是的,这种差异是/.实际上,应用规则所依据的方案与我们目前考虑的方案不同。

但是,好吧,我们如何参数化可能的方案呢?让我们回到上一节开始的组合子表达式:

年代
& # 10005

清楚(s、k);(年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]]

以下是此表达式中可能匹配项的位置:

位置
& # 10005

位置[[年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]], s [_][_][_] | k [_] [_]]

评估方案必须定义一种方法来说明在每个步骤中实际要做哪些匹配。一般来说,我们可以用任何算法来确定它。但一种方便的方法是根据特定的条件对位置列表进行排序,然后例如使用第一种方法k位置在结果中。

给定一个职位列表,可以使用两种明显的排序标准:一种基于职位说明的长度,另一种基于职位说明的内容。例如,我们可以选择(as)排序默认情况下是),先对较短的位置说明进行排序:

排序
& # 10005

排序({{0,0,0,1,1,0,1},{0,0,0,1,1},{0,0,0,1},{0},{1,0,0,1},{1}})

但较短的头寸规格对应的是什么?它们是组合子表达式中更“外部”的部分,位于树的更高位置。当我们说我们使用“最外层”的评估方案时,我们的意思是,我们首先考虑的是树中更高的匹配。

给定两个相同长度的位置规范,我们需要一种方法来比较它们。一个明显的方法是字典法——0在1之前排序。这个对应于取f之前xf (x),或先取最左边的对象。

我们必须决定是先按长度排序,然后按内容排序,还是反过来。但如果我们列举所有的选项,我们得到的结果如下:

allschemes
& # 10005


            

下面是表达式树中每个方案的第一个匹配:

allschemes
& # 10005


            

那么,如果我们在组合进化中使用这些方案会发生什么呢?下面是终止示例的结果s [s] [s] [s [s [s]]] [k] [s]如上所述,始终只保留给定排序条件的第一个匹配项,并在每个步骤中显示匹配项应用的位置:

allschemes
& # 10005


            

下面是如果我们允许每个排序列表的前2个匹配被应用的结果:

allschemes
& # 10005


            

以下是最左最外的结果,每一步最多允许1到8个更新:

CloudGet
& # 10005


            

这里是不同评估方案下的“到达定点的时间”表,每一步的更新数量不同:

allschemes
& # 10005


            

不出所料,到达固定点的时间总是随着每一步可以完成的更新数量的增加而减少。

对于稍微简单一点的终止示例(年代[s [s]]] [s] [s] [s](S (SS))瑞士),我们可以在每个不同方案的每个步骤中明确地查看树上的更新:

allschemes
& # 10005


            

那么不终止的组合子表达式呢?这些不同的评估方案会做什么?这里是结果s[s[s][s][s][s][s]年代sss (SS)),在每个情况下,每个步骤只使用一个匹配:

allschemes
& # 10005


            

如果我们允许在每个步骤中连续使用更多匹配项(以最左边最外层的顺序选择),则会发生以下情况:

组合进化图
& # 10005


            

不出意料的是,允许配对的数量越多,体型增长的速度就越快。连续极限”或“平均场理论,用于组合子的进化):

CombinatorEvolveList
& # 10005


            

查看不同更新方案的连续步骤的大小比率是很有趣的s[s[s][s][s][s][s]).一些方案比其他方案导致更“明显简单”的长期行为:

allschemes
& # 10005


            

事实上,只需更改允许的匹配数(此处为最左侧最外侧)即可产生类似的效果:

CombinatorEvolveList
& # 10005


            

那么其他的组合子表达式呢?不同的更新方案可能导致完全不同的行为。这是[s[s][s][s[s]][k](SS)年代(S (SS)) K):

allschemes
& # 10005


            

这里是(年代[s]] [s] [s] [s] [s [k]](SS)瑞士(SK))——对于某些更新方案来说,这提供了纯粹的周期性行为(没有k在原始组合子表达式中):

allschemes
& # 10005


            

值得注意的是——至少当有的时候k的不同更新方案甚至可以改变特定组合子表达式的求值是否终止。这种情况不会发生在8码以下。但是在8号的情况下,这是发生的事情s [s] [s] [s [s]] [s] [s] [k]SSS (SS)构造论):

allschemes
& # 10005


            

对于某些更新方案,它达到一个不动点(总是s [k]),但对另一些人来说,却是无限的成长。最内部的方案最糟糕,因为“缺少固定点”;他们为16个大小为8的组合子表达式做了这个。但是(正如我们前面提到的)最左最外有一个重要的特性,即如果存在固定点,它将不会错过固定点——尽管有时会冒着从一个过于笨重的路径到达固定点的风险。

但如果要在实践中应用类似组合器的转换规则,最好的方案是什么?Wolfram语言/.ReplaceAll)该操作实际上使用了最左边的最外层方案,但有一个重要的缺点:它不只是使用一个匹配,而是使用尽可能多的非重叠匹配。

再次考虑组合子表达式:

年代
& # 10005

(年代[s] [s] [s [s] [k [k] [s]] [s]]] [s] [s] [s [k [s] [k]] [k] [s]]

按最左边最外侧的顺序,此处可能的匹配项为:

CombinatorMatches
& # 10005


            

但关键是位置{0}的匹配与位置{0,0,0,1}的匹配重叠(即它是它的树祖先)。一般情况下,可能的匹配位置形成一个部分有序集,如下:

ReverseGraph
& # 10005


            

一种可能是总是在部分顺序的“底部”使用匹配——或者换句话说,最里面的匹配。这些匹配不可避免地不能重叠,所以它们总是可以并行进行,从而产生一个“并行最内层”的评估方案,该方案可能更快(尽管存在根本找不到固定点的风险)。

什么/.Does是有效地使用(按最左边的顺序)出现在部分顺序的“顶部”的所有匹配。其结果是总体更新速度更快。在s [s] [s] [s [s]] [s] [s] [k]上面的例子,反复应用/.(这是什么/ /。Does)在23步中找到固定点,而它只需要30步从左到右逐个替换——在这种情况下,并行最内层不终止:

ListStepPlot
& # 10005


            

s [s] [s] [s [s [s]]] [k] [s]KS SSS (S (SS))) parallel innermost does terminate,与for的26步相比,得到一个结果需要27步/.-但是使用较小的中间表达式:

CombinatorStep
& # 10005


            

但是,对于没有固定点的情况,/.往往会导致更快的增长。例如,使用s[s[s][s][s][s][s]年代sss (SS))它基本上给出纯指数2t/2生长(最终,parallel innermost也是如此):

ListStepPlot
& # 10005


            

一种新的科学我给出了一些组合子的结果/.正如我们在这里看到的,在“野外的组合子”中发现了很多相同的行为。

但是,好的,我们已经有了更新方案/.(以及它的重复版本/ /。),我们已经有了自动求值的更新方案(有或没有带“hold”属性的函数)。但是否有其他可能有用的更新方案,如果有,我们如何参数化它们?

我从一开始就在想这个问题设计开关电源——Mathematica和Wolfram语言的前身——已经有40多年的历史。出现问题的一个地方是递归定义的函数的自动求值。假设有一个阶乘函数定义为:

f
& # 10005

f [1] = 1;F [n]:= n [n - 1]

如果一个人要求,会发生什么f [0]?采用最明显的深度优先评估方案,进行评估f[-1]f [2]等等,永远不会注意到所有的东西最终都会乘以0,所以结果会是0。如果不是使用自动求值/ /。一切都会很好,因为它使用了不同的评估顺序:

f
& # 10005

f[0] / /。F [n_] -> n [n - 1]

让我们考虑一下斐波那契数列的递归定义(为了让它更明显地像“组合子”,我们可以使用它)构造而不是+):

f
& # 10005

if [1] = 1; if [1] = 1;F [n]:= F [n - 1] + F [n - 2]

如果你要求f [7]你需要对这棵树进行评估

清晰的
& # 10005


            

但问题是:你是怎么做到的?最明显的方法是对树进行深度优先扫描,然后进行处理ϕn但是如果你反复使用/.相反,你需要做更多的广度优先扫描,这需要更多的时间On2)计算:

FixedPointList
& # 10005

FixedPointList[# /。{[1] - > 1, f [2] - > 1, f [n_] - > [n - 1] + f (n - 2)} &、f [7]]

但是我们如何将这些不同的行为参数化呢?从现代的角度来看Wolfram物理项目,这就像选择不同的叶状结构——或不同的参照系——在描述一个结果对其他结果的依赖性的因果图中。在相对论中,有一些由速度参数化的类标准参考系惯性系。但一般来说,“描述合理的参考框架”并不容易,我们通常只讨论命名指标(史瓦西,克尔,…),就像我们在这里讨论的“命名更新订单”(“最左最内”,“最外最右”,…)。

但回到1980年,我确实有一个想法,至少是评估顺序的部分参数化。这是第3.1节SMP的文档

SMP的文档

我所谓的投影就是我们现在所说的函数;“过滤器”就是我们现在所说的参数。但基本上,这是说,通常情况下,一个函数的参数被计算(或“简化”在SMP的说法)之前,函数本身被计算。(不过请注意关于“未来并行处理实现”的提前转义句,它可能会异步地计算参数。)

但有趣的是:SMP的功能也有Smp矩形属性(大致是现代的“属性”),它们决定了如何进行递归计算。第一个近似的概念是Smp会在最里面和最外面之间选择,但在最里面的情况下,矩形会告诉你在“到达最外层”之前要走多少层。

是的,似乎没有人(包括我)真正懂得如何使用这些东西。也许有一种自然且易于理解的方法来参数化评估顺序(超越/.vs. Wolfram语言中的自动求值vs.保持属性机制),但我从未找到它。在这里看到与组合子的不同更新方案相关的所有复杂性并不令人鼓舞。

顺便提一下,总有一种方法可以完全指定求值顺序:只需做一些类似于过程编程的事情,其中每个“语句”都被有效地编号,并且可以是显式的转到,它表示下一步要执行的语句。但在实践中,这很快就会变得非常繁琐和脆弱——函数式编程的一个伟大价值在于,它通过隐式地由函数的求值顺序决定“执行顺序”来简化事情(是的,像这样的事情)也可用)。

一旦通过函数求值顺序确定了“执行顺序”,事情就会立即变得可扩展得多:无需指定任何其他内容,就会自动定义要做什么,例如,当一个人得到一个具有更复杂结构的输入时。仔细想想,什么时候递归通过表达式的不同部分以及什么时候递归通过重新求值有很多复杂的问题。但好消息是,至少Wolfram语言的设计方式,在实践中,事情通常“只是工作”,人们不需要考虑它们。

Combinator求值是一个例外,如我们所见,求值顺序的细节可能会产生重要影响。这种依赖性实际上与为什么很难理解组合子的工作原理有关。但是,研究组合子评估再次启发了一个人(至少是我),试图为评估顺序找到方便的参数化——也许现在是利用物理学的思想和直觉。

S Combinator的世界

在组合子的定义中年代k

{年代
& # 10005

[x_][y_] -> x[z][y[z]], k[x_][y_] -> x}

年代基本上是“建立东西”,而K就是“削减开支”。从历史上看,在用组合符创造和证明事物时,平衡两者是很重要的年代K.但我们上面看到的已经很清楚了年代一个人已经可以做一些相当复杂的事情。

所以考虑组合子的最小情况是很有趣的年代.的大小n(即。LeafCountn),有

加泰罗尼亚努伯
& # 10005

CatalanNumber (n - 1) = =二项(2 n + 1, n + 1) / (2 n + 1)

(~对于大型n),每一个组合子都可以简单地根据它所涉及的括号开和闭的顺序来描述。

这些组合子中的一些会在有限的时间内终止,但在大小为7的组合子中,还有一些不会终止:

ttimes
& # 10005


            

而且已经有一些奇怪的东西了:非终止组合子表达式的比例随着大小稳定地增加,然后急剧下降,然后又开始上升:

ttotals
& # 10005


            

但是,让我们先看看求值终止的组合子表达式。顺便说一下,当我们处理年代单独来看,不存在某些评估方案终止或其他方案不终止的可能性:它们要么全部终止,要么没有终止。(这一结果是在20世纪30年代建立的年代combinator-unlikeK——实际上是“保守变量”,使它成为所谓的一个例子λ微积分。)

对于最左侧最外侧的评估,以下是停止时间分布,显示出大致呈指数衰减且逐渐展宽:

ttimes
& # 10005


            

下面是(最左外的)“champion”——在终止前存活时间最长(计算值最左外的)的组合子表达式:

CombinatorTraditionalForm
& # 10005


            

存活时间(又称停顿时间)随着大小大致呈指数增长,明显比我们在实验中看到的要慢得多SK例:

ListStepPlot
& # 10005


            

冠军们实际表现如何?下面是大小序列的情况:

组合进化图
& # 10005


            

体型逐渐增大,然后啪嗒啪嗒:进化终止。看看详细的行为(这里的大小为9的“右关联渲染”),显示出正在发生的事情是非常系统的:

组合进化图
& # 10005


            

这种差异再次反映了这种行为的系统性:

SKCombinatorLeftmostOutermostLeafCounts
& # 10005


            

看起来,基本发生的事情是,组合子充当了一种数字计数器,经历了指数级的步骤,最终构建了一个非常规则的树状结构:

CombinatorExpressionGraph
& # 10005


            

顺便说一下,虽然最终状态是一样的,但不同的评估方案,其演变过程是不同的。例如,我们的“最左最外层冠军”在深度优先评估时实际上终止得更快:

组合进化图
& # 10005


            

不用说,深度优先(也就是从左到内)的冠军可能是不同的,尽管有些令人惊讶的是,有些是相同的(但不是大小8、12、13):

组合固定点列表
& # 10005


            

如果我们看看多路图,就能知道所有可能的评估方案会发生什么。这是尺寸为8的最左边的冠军的结果(年代[s [s]]] [s] [s] [s]

MWCombinatorGraphMinimal
& # 10005


            

在多路图中,连续层次上的表达式数量开始呈指数增长,但在12步之后,它迅速下降——最终生成一个有74个节点的有限图(最左最外是“最慢”的计算方案——最多需要15步):

多路组合器
& # 10005


            

即使对于尺寸为9的人来说,完整的多路图也太大了,无法显式地构造。经过15步之后,节点的数量已经达到6598个,并且看起来越来越接近-即使在最多86步之后,所有“悬垂端”都必须解决,并且系统必须达到其固定点:

MWCombinatorGraphMinimal
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[MWCombinatorGraphMinimal[s[s]][s[s] [s[s] [s][s], 12, NodeSizeMultiplier -> 1.5], AspectRatio -> 1]

会发生什么,年代不终止的组合表达式?我们已经在上面看到了一些人们观察到的大小增长的例子(比如最左边最外面的求值)。下面是一些具有大致指数行为的例子,在对数刻度上显示了连续步骤之间的差异:

ListStepPlot
& # 10005


            

以下是一些线性尺度上的差异:

ListStepPlot
& # 10005


            

有时有相当长的瞬变,但值得注意的是,在所有的8629个大小为11的无限增长组合子表达式中,没有一个演化在总体大小上显示出长期的不规律性。当然,像30号规则这样的东西在总体大小上也不会显示出不规则;人们必须从“内部”看到复杂的行为,而可视化的困难使得在组合符的情况下很难系统地做到这一点。

但是,从上面的图片来看,似乎有“有限数量的方式”可以让组合表达式无限制地增长。有时,我们很容易看到无限增长是如何发生的。这里有一个特别的“纯粹的游戏”示例:大小为9的情况(年代[s [s]]] [s [s [s]]] [s [s]](年代(SS)) ((SS)) (SS)),与所有评估方案的发展方式相同(图中,每一步的比赛根都突出显示):

CombinatorExpressionGraph
& # 10005


            

看看每个匹配项下面的子树

CombinatorExpressionGraph
& # 10005


            

很明显,有一个明确的进展,将永远持续下去,导致无限的增长。

但是如果我们看看相应的子树序列比如最小的无限增长组合子表达式[s][s][s][s][s]SSS党卫军(SS)),就不那么明显了:

CombinatorExpressionGraph
& # 10005


            

但有一个相当重要的问题20世纪90年代末取得了显著的成果这就给了我们一种“计算”组合子表达式的方法,并告诉我们它们是否会导致无限增长特别是能够从一个初始的组合子表达式中直接说出它是会永远发展下去,还是会达到一个固定点。

首先要写一个组合子表达式,比如(年代[s [s]]] [s [s [s]]] [s [s]](年代(SS)) ((SS)) (SS)),以明确的“功能”形式:

FunctionToApplication
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);FunctionToApplication [s [s [s [s]]] [s [s [s]]] [s [s]]] /。应用程序- > f

然后一个想象f (x, y)作为一个具有显式(比如整数)值的函数。一个代替年代通过显式的值(比如一个整数),然后定义的值f [1]f [1, 2]等。

作为第一个例子,我们假设s = 1f[x_u,y_u]=x+y.然后我们可以对上面的组合子表达式“求值”为

SCombinatorAutomatonTreeGeneral
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);SCombinatorAutomatonTreeGeneral[s[s[s]] [s[s] [s[s] [s[s], Application[x_, y_] -> x + y, 1, VertexSize -> .6]

在这种情况下,根的值只是计算总大小(即。LeafCount).

但通过改变f我们可以探索组合表达式树的其他方面。2000年发现的是,有一种完整的方法可以通过设置39个可能的值,然后f (x, y)是一个特殊(“树自动机”)“乘法表”对于这些值:

MapIndexed
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);gridColors(间):=混合[{色调(0.1,0.89,0.984),颜色(0.16,0.51,0.984),色调[0.04768041237113402,0,0.984]},x)网格[MapIndexed[如果[# 2[[1]]= = = 1 | | # 2[[2]]= = = 1项(风格(# 1、9、粗体,灰度。35]],背景-> GrayLevel[。9]],如果[# 1 = = 38岁的项(“”,背景——> RGBColor (0.984, 0.43, 0.208), FrameStyle - >暗(RGBColor(0.984, 0.43, 0.208)。2]],项[风格(# 1 9灰度[0。6]],背景——> gridColors [(38 - 1) / 38], FrameStyle - >暗(RGBColor(0.984, 0.43, 0.208)。2]]]]&,预谋[MapIndexed[平(预谋(# 1,# 2 - 1]]&、表[i \[应用程序]j /。maintablesw,{0,我38},{0 j, 38}]],平(预谋[[0,38]," \[应用程序]"]]],{2}],间距- >{。25, 0}, ItemSize -> {1,1}, Frame -> All, FrameStyle -> GrayLevel[。6], BaseStyle -> "Text"]

亮红色(值38)代表无限生长的种子的存在——一旦存在,f使其传播到树的根。使用此设置,如果年代的价值0,上面的组合子表达式可以被“计算”为:

SCombinatorAutomatonTree
& # 10005

CloudGet[”https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl“];sCombinatorAutomatoEntree[s[s]][s[s]][s[s]]]s[s]],顶点大小->0.5]

在进化的连续步骤中,我们得到:

SCombinatorAutomatonTree
& # 10005


            

或经过8步:

SCombinatorAutomatonTree
& # 10005


            

“最低38”总是在匹配发生的子树的顶部,作为这个子树是无限增长的种子这一事实的“见证”。

以下是一些大小为7的组合子表达式,展示了如何识别这两个导致无限增长的组合子:

SCombinatorAutomatonTree
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);标记[SCombinatorAutomatonTree [#, VertexSize - > 5,图象尺寸- >{自动200}],风格[文本[#],12]]& / @ {s [s] [s] [s] [s] [s] [s], s [s [s] [s] [s] [s]] [s], s [s [s]] [s] [s] [s] [s], s [s] [s] [s [s]] [s] [s], s [s [s [s]] [s [s]]] [s], s [s] [s [s [s] [s]]] [s], s [s [s [s]]] [s [s [s]]]}

如果我们处理的是包含这两个的组合子表达式年代K我们知道,一般来说,一个特定的表达式是否会停止是无法确定的。有一种确定的方法来确定一个表达式是否只包含年代停止吗?

有人可能会认为这是年代单独在计算上是微不足道的。但这个问题还有更多的原因。在过去,人们常常认为“计算”必须从某个初始(“输入”)状态开始,然后在一个与最终结果相对应的固定点结束。但这肯定不是现代计算机实际工作的方式。当一个特定的计算完成时,计算机及其操作系统并不完全停止。相反,计算机会继续运行,但用户会收到一个信号,来查看为计算提供输出的东西。

在这样的设置中计算通用性并没有本质上的不同;这只是一个“部署”问题。事实上,关于细胞自动机和图灵机通用性的最简单的例子已经用这种方法证明了。

那么这是如何工作的呢年代组合表达式?基本上,任何复杂的计算都必须存在于无限组合增长过程之上。或者,换句话说,计算必须以某种潜在无限长的“瞬态”形式存在,实际上“调节”无限增长的“载体”。

人们可以通过从导致无限增长的无限集合中选择适当的组合子表达式来设置程序。然后,组合子表达式的演化将“运行”程序。人们会使用一些计算有界的过程(也许是树自动性的有界版本)来识别计算结果何时已经完成——人们会使用一些计算有界的“解码器”来“读出”结果。

我在计算世界的经验计算等价原则一旦一个系统的行为不是“明显的简单”,系统将能够进行复杂的计算,特别是计算通用。的年代Combinator是一种奇怪的边缘案例。至少从我们在这里看到的方式来看,它的行为并不“明显简单”。但我们还没能识别出在系统中出现的看似随机的行为30规则,这是复杂计算的一个标志,可能计算普遍性

实际上有两种基本的可能性,一种是年代只有Combinator能够进行复杂的计算,例如,在确定一个长期的结果时,存在计算不可约性年代combinator进化。或者是年代combinator从根本上说是计算上可简化的,有一些方法(也许还有一些数学上的新方向)可以“打开它”,并允许一个人很容易地预测年代组合子表达式就可以了。

我不确定它将如何发展——尽管我在过去四十年中几乎一致的经验是,当我认为某个系统“太简单”而不能“做任何有趣的事情”或显示复杂的计算时,它最终会证明我错了,通常是以奇怪和意想不到的方式。(就……而言年代组合子,一种可能性,就像我在登记机——复杂的计算可能首先会在非常微妙的效果中显现出来,比如看似随机的逐行模式。)

但无论发生什么,令人惊讶的是,在发明年代combinator关于它仍然有这么大的谜团。在里面他最初的纸,摩西Schönfinkel表达了他的惊讶,这样简单的东西年代K足以实现我们现在所说的通用计算。事实上,如果一个人能走得更远,这将是非常了不起的年代单独是足够的:一个最小的通用计算的例子隐藏在普通的视线100年。

(顺便说一下,除了具有特定评价方案的普通“确定性”组合子进化,还可以考虑“不确定性”案例对应于多路图中所有可能的路径。在这种情况下,有一个问题是如何对无穷多的图进行分类年代组合表达式可能是超限数形式的。)

因果图与组合子的物理化

不久前,人们还没有任何理由认为来自物理学的想法会与组合符有关。但是我们的Wolfram物理项目已经改变了。事实上,似乎我们的物理项目的方法和直觉——以及它们与相对论等事物的联系——可能会给组合子带来一些有趣的新见解,实际上可能会让它们的操作变得不那么神秘。

在我们的物理项目中,我们想象宇宙是由大量的抽象元素(“空间原子”)组成的,这些元素之间通过各种关系联系在一起——就像用超图表示的那样。宇宙的行为——以及时间的进展——随后与根据一套特定的(大概是局部的)规则重复重写这个超图联系在一起。

这当然与组合符的工作方式不同,但有明确的相似之处。在组合子中,基本的“数据结构”不是超图,而是二叉树。但是组合子表达式是根据树上的局部规则,通过重复重写树来进化的。

有一种中间情况,我们经常把它用作物理(尤其是量子力学)的玩具模型:弦替代系统。一个组合子表达式可以写成“线性”(如s [s] [s] [s [s [s]]] [k] [s]),但实际上它是树状结构和层次结构。在一个字符串替换系统但是,一个系统只有普通字符串,由字符序列组成,没有任何层次结构。然后,系统通过应用一些局部字符串替换规则反复重写字符串来进化。

例如,可以有一个规则,如{" a "" BBB "、" BB "“A”}。就像组合符一样,给定一个特定的字符串,比如“BBA”,在哪里应用该规则有不同的可能选择。与组合器一样,我们可以构建一个多路图来表示所有可能的重写序列:

多路系统
& # 10005

图[ResourceFunction[“MultiwaySystem”][{“A”->“BBB”,“BB”->“A”},“BBA”,5,“StatesGraph”],AspectRatio->1]

同样,与组合器一样,我们可以定义一个特定的“评估顺序”,该顺序确定在每一步对字符串应用哪些可能的更新,并定义通过多路图的路径。

对于字符串来说,“最内层”和“最外层”的概念并不相同,但有“最左层”和“最右层”。在这种情况下,最左边的更新将给出进化的历史

嵌套列表
& # 10005

NestList [StringReplace[#,{“A”- >“BBB”,“BB”——>“A”},1]&、“BBA”,10]

对应路径:

多路系统
& # 10005

与[{图g = [ResourceFunction [" MultiwaySystem "][{“A”- >“BBB”,“BB”——>“A”},“BBA”,5,“StatesGraph”],AspectRatio - > 1)}, HighlightGraph (g,风格(子图(g, NestList [StringReplace[#,{“A”- >“BBB”,“BB”——>“A”},1]&、“BBA”,10]],厚,RGBColor [0.984, 0.43, 0.208]]]]

这是与那条路径相对应的潜在进化,更新事件用黄色表示:

置换系统因果图
& # 10005

ResourceFunction[“SubstitutionSystemCausalPlot”][ResourceFunction[“SubstitutionSystemCausalEvolution”][{“A”->“BBB”,“BB”->“A”},“BBA”,8,“First”],“CellLabels”->“True”,ColorTable”->{色调[0.6296304159168616,0.13,0.9400000000000001],色调[0.6296304159168616,0.07257971950090639,0.9725485374,1.],图像大小->120]

但现在我们可以开始追踪一个事件对另一个事件的“因果依赖性”。为了向新事件提供“输入”,需要从之前的事件中生成哪些字符作为“输出”?让我们看一个有更多事件发生的例子:

置换系统因果图
& # 10005

ResourceFunction [" SubstitutionSystemCausalPlot "] [BlockRandom [SeedRandom [33242];ResourceFunction["SubstitutionSystemCausalEvolution"][{"A" -> "BBB", "BB" -> "A"}, "BBA", 8, {"Random", 3}]], "CellLabels" -> True, "ColorTable" -> {Hue[0.6296304159168616, 0.13, 0.9400000000000001], Hue[0.6296304159168616, 0.07257971950090639, 0.9725480985324374, 1。}, ImageSize -> 180]

但现在我们可以画一个因果图因果关系事件之间,即哪些事件必须发生,才能启动后续事件:

置换系统因果图
& # 10005

使用[{gr = ResourceFunction["SubstitutionSystemCausalPlot"][BlockRandom[seerandom [33242];ResourceFunction[“SubstitutionSystemCausalEvolution”][{“A”- >“BBB”,“BB”——>“A”},“BBA”8日{“随机”,3}]],“CausalGraph”——>真的,“CausalGraphStyle”——>指令(厚,红),“ColorTable”- >{色调(0.6296304159168616,0.13,0.9400000000000001),颜色(0.6296304159168616,0.07257971950090639,0.9725480985324374,1。}, preend [gr[[2;;]}, preend [gr[[2;;1]],取代[gr[[1]],箭头 [__] -> {}, ∞)~加入~ {RGBColor(0.984, 0.43, 0.208),厚度(0.01),病例(gr,箭头(__),∞)}]]

在物理层面上,如果我们是一个观察者嵌入系统中,按照系统的规则运行,我们最终能“观察”的只是“无实体”。因果图,其中节点是事件,边表示这些事件之间的因果关系:

SubstitutionSystemCausalGraph
& # 10005

ResourceFunction["SubstitutionSystemCausalGraph"][{"A" -> "BBB", "BB" -> "A"}, "BBA", 5]

那么这和组合子有什么关系呢?好吧,我们也可以为它们创建因果图,以获得在组合者进化过程中“发生了什么”的不同观点。

对于组合子系统来说,“因果关系”的定义有一个非常微妙的地方(复制的子树什么时候“不同”?等)。在这里,我将使用一个简单的定义,它将告诉我们组合子中的因果关系是如何工作的,但这需要进一步改进,以适应我们想要的其他定义。

假设我们用线性的方式写出了组合子表达式。下面是组合子的演变:

组合进化图
& # 10005


            

为了理解因果关系,我们需要跟踪“什么被重写为什么”——以及给定的重写事件“从”哪个先前的重写事件“获取其输入”。从树的角度来看上面的重写过程是很有帮助的:

CombinatorExpressionGraph
& # 10005


            

回到文本表示法,我们可以用“状态”和连接它们的“事件”来展示进化。然后我们可以追踪(用橙色表示)这些事件之间的因果关系:

多路组合器
& # 10005


            

继续这个步骤,我们得到:

多路组合器
& # 10005


            

现在只保留因果图,继续到组合子进化结束,我们得到:

CombinatorCausalGraph
& # 10005


            

将此与总结一系列改写事件的情节相比较是很有趣的:

组合进化图
& # 10005


            

那么我们在因果图中看到了什么呢?基本上,它向我们展示了系统中发生了什么“评估线程”。当组合子表达式的不同部分实际上独立更新时,我们看到多个因果边并行运行。但当有一个同步评估影响整个系统时,我们只看到一个线程——一个单一的因果边。

因果图在某种意义上是对组合进化结构的总结,去掉了很多细节。即使组合表达式的大小快速增长,因果图仍然可以保持相当简单。例如,不断增长的组合[s][s][s][s][s]有一个因果图,形成一个线性链与简单的“边环”,系统地进一步分离:

CombinatorCausalGraph
& # 10005

CloudGet[”https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl"]; 组合因果图[s[s][s][s][s][s],40,{“最左边的”,“最外面的”,1},AspectRatio->3]

有时,由于组合子系统的不同部分彼此之间发生了因果性的断开,增长似乎消失了:

CombinatorCausalGraph
& # 10005


            

以下是一些其他示例:

CombinatorCausalGraph
& # 10005


            

但是,这种因果图依赖于所使用的评估方案吗?这是一个微妙的问题,它敏感地依赖于抽象表达式及其子表达式的同一性定义。

首先要说的是,组合子是合流的,从这个意义上讲,不同的评估方案即使采用不同的路径,在组合子表达式的演化终止时,也必须始终给出相同的最终结果。与此密切相关的是,在组合子系统的多向图中,任何分支都必须伴随着随后的合并。

对于字符串和超图重写规则,这些属性的存在与我们称之为因果不变性的另一个重要属性相关联。而因果不变性正是由不同更新顺序生成的因果图必须始终同构的性质。(在我们的物理模型中,这就是导致相对论不变性、广义协方差、量子力学中的客观测量等的原因。)

那么对组合子来说也是这样吗?它是复杂的。字符串和超图重写系统都有一个重要的简化特性:当你更新其中的内容时,你可以合理地认为你所更新的内容已经被更新事件“完全消耗”了,而一个“全新的东西”将作为事件的结果而被创造出来。

但对于combinators来说,这并不是一个合理的描述。因为当有更新事件时,比如s [x] [y] [z]x可能是一棵巨大的子树,你最终只是“复制”,而没有“消费”和“重组”。在字符串和超图的例子中,在“涉及到更新”的系统元素和没有涉及到更新的系统元素之间有明显的区别。但在组合器系统中,“只是复制”的子树中埋得很深的节点是否应该被视为“涉及”并不明显。

在构造多路图时,定义之间存在复杂的相互作用。考虑一个字符串重写系统。从一个特定的状态开始,然后以所有可能的方式应用重写规则:

LayeredGraphPlot
& # 10005

LayeredGraphPlot[ResourceFunction["MultiwaySystem"][{"A" -> "AB", "BB" -> "A"}, "A", 5, "EvolutionGraphUnmerged"], AspectRatio -> .4]

如果没有其他任何东西,这只会生成一个结果树。但多路图背后的关键思想是,当状态相同时,它们应该合并,在这种情况下给出:

图
& # 10005

图(ResourceFunction[“MultiwaySystem”][{“A”- >“AB”、“BB”——>“A”},“A”,5,“StatesGraph”],AspectRatio——>。4)

对于字符串来说,“相同”的含义很明显。对于超图,自然的定义是超图同构。那么组合子呢?它是纯粹的树同构,还是应该考虑子树的“起源”?

(还有一些问题,如是否应该根据“瞬时状态”来定义多路图中的节点,或者是否应该基于“迄今为止的因果图”,即通过特定事件历史获得的图。)

这些都是微妙的问题,但似乎很清楚,有了适当的定义,组合子将显示因果不变性,因此(适当定义的)因果图将独立于评估方案。

顺便说一下,除了构建特定进化历史的因果图,我们还可以构建多路因果图代表所有可能的因果关系,在不同的历史分支内部和之间。这显示了的(终止)演化的多路图s[s][s][s[s[k]][k],带有随意的边注:

多路组合器
& # 10005

ResourceFunction["MultiwayCombinator"][{s[x_][y_][z_] -> x[z][y[z]], k[x_][y_] -> x}, s[s][s [s[k]] [k], 15, "EvolutionCausalGraphStructure", GraphLayout -> " layereddigraphembed ", AspectRatio -> 2]

这里是多元因果图

多路组合器
& # 10005

ResourceFunction[“MultiwayCombinator”][{年代(间)[y_] [z_] - > x [z] [y [z]], k(间)[y_] - > x}, s [s] [s] [s [s [k]]] [k], 15日“CausalGraphStructure AspectRatio - > 1)

(是的,这些定义在这里并不是完全一致的,所以可以在这里提取的因果图的个体实例并不都是相同的,正如因果不变性所暗示的那样。)

多维因果图s[s[s][s][s][s][s]显示了因果边缘的真实爆发:

多路组合器
& # 10005

ResourceFunction[“MultiwayCombinator”][{年代(间)[y_] [z_] - > x [z] [y [z]], k(间)[y_] - > x}, s [s [s]] [s] [s] [s] [s], 17日“CausalGraphStructure AspectRatio - > 1, GraphLayout - >“LayeredDigraphEmbedding”)

在我们的物理模型中,因果图可以被认为是时空结构的一种表示。接踵而来的事件是“时间型分离”。那些可以被安排成没有时间型分隔的事件可以被认为形成“空间型切片”(或“同时性表面”),并被空间型分隔。(因果图的不同层次对应于不同的“参考框架”,并在相同的空间切片中识别出不同的事件集。)

当我们处理多路系统时,事件也有可能与不同的“历史线程”相关联,从而像分支一样分离。但在组合系统中,还有另一种可能的事件分离形式——我们可以称之为“树状分离”。

考虑以下两对更新事件:

CombinatorExpressionGraph
& # 10005


            

在第一种情况下,事件实际上是“类空分离”的。它们通过在同一个组合表达式中连接,但不知何故它们出现在“不同的地方”。但是第二种情况呢?同样,两个事件通过在同一个组合表达式中连接。但现在它们不是真正的“在不同的地方”;它们只是树上“不同的鳞片”。

超图重写系统的一个特征是,在大规模极限下,它们生成的超图可以表现为连续流形,潜在地表示物理空间,超图距离近似几何距离。在组合子系统中,几乎不可避免地存在一种嵌套结构,这种结构可能会让人联想到比例不变的关键现象和概念,比如比例相对论。但我还没有看到组合子系统的极限行为产生类似有限维“类流形”空间的东西。

在组合子因果图中看到“事件视界”是很常见的,在这种情况下,组合子系统的不同部分实际上变得因果断开。当组合符达到固定点时,就好像“时间结束了”——就像它在做的那样时空中的类空间奇点.但组合子系统中存在新的“树状”极限现象,这可能反映在双曲空间的性质中。

字符串和超图重写系统的一个重要特征是,它们的规则通常被假定为局部的,因此任何给定元素的未来影响必须在一定的“影响锥”内。或者,换句话说,有一个光锥它定义了事件的最大空间分离,当它们有一个特定的时间分离时,这些事件可以被因果联系起来。在我们的物理模型中,还有一个“纠缠锥”,它定义了事件之间的最大分枝分离。

那么在组合子系统中呢?这些规则并不是真正的“空间局部”,而是“树局部”。因此,它们的“树锥”影响力有限,与“最大树状速度”相关——或者,从某种意义上说,规模变化的最大速度。

基于字符串、超图和组合子表达式的重写系统都具有不同的简化和复杂特性。底层元素(“按顺序排列的字符”)之间的关系对于字符串来说是最简单的。对于超图来说,什么算作相同元素的概念是最简单的。但是对于组合子表达式来说,“元素标识符”之间的关系可能是最简单的。

回想一下,我们总是可以用DAG来表示一个组合子表达式,在DAG中我们“从原子构建”,一路共享公共子表达式:

CombinatorToDAG
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[组合todag [s[s]][s][s][s], VertexLabels ->放置[自动,自动,ToString]]

但是在这个表示中,组合进化是什么样子的呢?让我们从一个非常简单的例子开始k [x] [y],一步就变成了x.以下是我们如何在dag中呈现这种进化过程:

图
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs。\西城”);图[#,VertexLabels -> Placed[Automatic, Automatic, ToString], GraphLayout -> " layereddigraphembeds "] & /@ SKDAGList[k[x][y], 1]

第二个DAG中的虚线表示更新事件,在本例中,该事件将转换k [x] [y]“原子”x

现在让我们考虑s [x] [y] [z].这里还有一条虚线表示进化:

图
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[#,VertexLabels -> Placed[Automatic, Automatic, ToString], GraphLayout -> " layereddigraphembeds "] & /@ SKDAGList[s[x][y][z], 1]

现在让我们再加一条:不要考虑k [x] [y]但是s[k[x][y]].外年代这里什么都不做。但它仍然需要解释,从某种意义上说,它必须被“包裹起来”x来自k [x] [y]x.我们可以用虚线表示的“树回拉伪事件”来表示“重新包装”过程:

图
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[#,VertexLabels -> Placed[Automatic, Automatic, ToString], GraphLayout -> " layereddigraphembeds "] & /@ SKDAGList[s[k[x][y], 1]

如果一个给定的事件发生在树的深处,那么就会有一系列“回拉伪事件”来“重建树”。

事情很快就变得相当复杂。这里是(最左最外)的进化(年代[s]] [s] [k] [s]到固定的dag点:

SKDAGList
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);SKDAGList[[年代[s]] [s] [k] [s], 5]

或使用标签:

图
& # 10005

CloudGet(“https://www.wolframcloud.com/obj/sw-blog/Combinators/Programs.wl”);图[Last[SKDAGList[s[s]][s][k][s], 5], VertexLabels ->放置[Automatic, Automatic, ToString]]

一个显著的特点是,从某种意义上说,这最后一个DAG以“最大共享”的方式编码了进化的完整历史通过这个DAG,我们可以构造一个因果图,它的节点是从DAG中表示更新事件和伪事件的边派生出来的。目前还不清楚如何以最一致的方式实现这一点,特别是在处理伪事件时。但这里有一个可能的因果图版本,用于(年代[s]] [s] [k] [s]到固定点,黄色节点表示事件,灰色节点表示伪事件:

Baidu