丰托里奥

2021-02-17 18:32:48

您可能已经听说过人们说函数式编程更具学术性,而实际的工程则以命令式进行。我将向您展示真正的工程是可行的,并且我将使用由工程师为工程师设计的计算机游戏来说明这一点。这是一个名为Factorio的模拟游戏,在其中您将获得探索,建立处理它们的工厂,创建越来越复杂的系统的资源,直到最终能够发射可能将您带离荒凉星球的太空船。如果这不是最纯粹的工程,那么我不知道这是什么。然而,玩此游戏时几乎所有您要做的事情都具有与功能编程相对应的功能,并且它可以用来教授编程的基本概念,并且在某种程度上还可以教授类别理论。因此,事不宜迟,让我们开始吧。

每种编程语言的构造块都是函数。一个函数接受输入并产生输出。在Factorio中,它们称为组装机或组装机。这是生产铜线的装配工。

如果显示有关汇编程序的信息,则会看到其使用的配方。这一个拿一个铜板,并产生一对铜线线圈。

该配方实际上是强类型系统中的功能签名。我们看到两种类型:铜板和铜线,以及它们之间的箭头。而且,对于每个铜板,组装人员都会生产一对铜线。在Haskell中,我们将此函数声明为

我们不仅具有用于不同组件的类型,而且可以将类型组合成元组-这里是同质对(CopperWire,CopperWire)。如果您不熟悉Haskell表示法,则可能是C ++中的样子:

它需要一对铁板,并产生一个铁齿轮。我们可以写成

许多食谱需要组合使用不同类型的成分,例如用于生产红色科学包装的成分

配对是产品类型的示例。 Factorio配方使用加号表示元组;我猜这是因为我们经常将总和读为“ this and this”,而“ and”则介绍了一种产品类型。汇编器需要两个输入才能产生输出,因此它接受产品类型。如果需要两者之一,我们将其称为求和类型。

我们还可以将两种以上的成分进行元组化,如本生产电子电路(或绿色电路,通常称为绿色电路)的配方中所述

现在假设您拥有原始的成分:铁板和铜板。您将如何生产红色科学或绿色电路?这是功能组合的开始。您可以将铜线组装器的输出作为输入传递给绿色电路组装器。 (您仍将必须用铁板将其记录下来。)

您从一个铜板和两个铁板开始。将铁板送入齿轮装配器。您将生成的齿轮与铜板配对,然后将其传递给红色的科学装配工。

Factorio中的大多数汇编程序都采用不止一个参数,因此我无法提出一个更简单的组合示例,而该示例不需要重新组合。在Haskell中,我们通常以咖喱形式使用函数(我们稍后会再介绍),因此在这里编写起来很容易。

合成也是类别的功能,因此我们应该问一个问题,是否可以将汇编程序视为类别中的箭头。它们的组成显然是缔合的。但是,我们有等同于身份的箭头吗?它需要某种类型的输入并将其返回原样。实际上,确实有一些称为插入程序的东西可以做到这一点。这是两个汇编器之间的插入器。

实际上,在Factorio中,您必须使用插入程序来直接编写汇编程序,但这只是实现细节(从技术上讲,插入标识函数不会改变任何内容)。

但是Factorio类别具有更多结构。如我们所见,它支持任意类型的有限乘积(元组)。这样的类别称为笛卡尔。 (稍后我们将讨论此产品的单位。)

请注意,我们已经将多个Factorio子系统标识为函数:汇编程序,插入程序,汇编程序的组合等。在编程语言中,它们都只是函数。如果我们要设计基于Factorio的语言(我们可以称其为Functorio),则需要将汇编程序的组成部分封装到一个汇编程序中,或者甚至制作一个需要两个汇编程序并产生其组成部分的汇编程序。那将是一个更高阶的汇编器。

功能语言的定义特征是使功能成为一流对象的能力。这意味着可以将函数作为参数传递给另一个函数,并可以将结果作为另一个函数的结果返回。例如,我们应该有一个生产汇编器的诀窍。而且确实有这样的配方。它所需要的只是绿色电路,一些齿轮和一些铁板:

如果Factorio一直是强类型语言,那么将有单独的配方来生成不同的汇编程序(即具有不同配方的汇编程序)。例如,我们可能有:

取而代之的是,配方产生一个通用的汇编程序,它使玩家可以在其中手动设置配方。在某种程度上,玩家提供了最后一种成分,即所有可能配方的枚举元素。该枚举显示为选项菜单:

由于我们已将插入程序标识为标识函数,因此我们也应该有一个生成它的方法。确实有一个:

我们是否还有以函数为参数的函数?换句话说,使用汇编程序作为输入的配方?实际上,我们这样做:

同样,此配方接受尚未分配其自己的配方的通用汇编程序。

这表明Factorio支持高阶函数,并且确实是一种功能语言。这里我们所拥有的是一种不仅将函数(汇编器)视为对象之间的箭头,而且还将其视为可以由函数生成和使用的对象的方法。在类别理论中,这样的物化箭头类型称为指数对象。以箭头类型表示为对象的类别称为封闭类别,因此我们可以将Factorio视为笛卡尔封闭类别。

函数编程的一个重要方面似乎在Factorio中已被打破。功能应该是纯净的:变异是禁忌。在Factorio中,我们一直在谈论汇编程序消耗资源。纯函数不会消耗其参数-您可以将相同的项目传递给许多函数,并且仍然存在。在包括纯功能语言在内的一般编程中,处理资源是一个真正的问题。幸运的是,有很多聪明的方法可以处理它。例如,在C ++中,我们可以使用唯一的指针并移动语义,在Rust中,我们拥有所有权类型,Haskell最近引入了线性类型。 Factorio的操作与Haskell的线性类型非常相似。线性函数是保证使用其参数的函数。 Functorio汇编器是线性函数。

Factorio全部涉及消耗和转换资源。资源来自矿山中的各种矿石和煤炭。也有一些树木可以砍成木头,还有诸如水或原油之类的液体。然后,您的行业会线性地消耗这些外部资源。在Haskell中,我们将通过将称为延续的线性函数传递给资源生产者来实现它。线性函数保证完全消耗资源(不泄漏资源),并且不制作同一资源的多个副本。这些是Factorio工业园区自动提供的保证。

当然,Factorio并非被设计为一种编程语言,因此我们不能指望它能够实现编程的各个方面。可以想象我们将如何将一些更高级的编程功能转换为Factorio,这很有趣。例如,curry如何工作?为了支持currying,我们首先需要部分应用。这个想法很简单。我们已经看到汇编程序可以被视为一流的对象。现在想象一下,您可以使用设定的配方来生产汇编程序(强类型汇编程序)。例如这个:

这是一个两输入的汇编器。现在给它一个铜板,在程序员看来,这叫做部分应用。部分原因是因为我们没有为其提供铁齿轮。我们可以将部分应用的结果视为一种新的单输入装配器,该装配器期望使用铁制齿轮并能够生产一个红色科学烧杯。通过部分应用功能makeRedScience

实际上,我们刚刚设计了一个过程,该过程赋予了我们一个(高阶)功能,该功能使用一块铜板并创建一个“底涂”组装机,该组装机只需要铁齿轮即可生产红色科学材料:

现在,我们想使这个过程自动化。我们想要一个带有两个输入的汇编器的东西,例如makeRedScience,并返回一个单个输入的汇编器,该汇编器会生成另一个“主”单输入器的汇编器。该野兽的类型签名为:

请注意,具体类型是什么都没关系。重要的是,我们可以将带有一对参数的函数转换为返回函数的函数。我们可以使其完全多态:

在这里,类型变量a,b和c可以替换为任何类型(特别是CopperPlate,Gear和RedScience)。这是一个Haskell实现:

到目前为止,我们还没有讨论过如何将参数(项目)传递给函数(汇编程序)。我们可以手动将项目放入汇编器,但是很快就会变得很无聊。我们需要使交付系统自动化。一种实现方法是使用某种容器:箱子,火车货车,桶或传送带。在编程中,我们称这些函子。严格来说,函子一次只能容纳一种类型的物品,因此,铁皮箱的类型应与齿轮箱的类型不同。 Factorio不会强制执行此操作,但实际上,我们很少在一个容器中混合使用不同类型的物品。

函子的重要属性是可以将函数应用于其内容。最好用传送带来说明。在这里,我们采用将铜板变成铜线的方法,然后将其应用于整个铜输送带(从右至右),以生产铜线输送带(向左)。

皮带可以携带任何类型的物品的事实可以表示为类型构造器-由任意类型a参数化的数据类型

您可以将其应用于任何类型以获取特定项目的皮带,例如

它是函子的事实通过实现多态函数mapBelt来表达

该函数取函数a-> b,并产生将as的带转换成bs的带的函数。因此,要创建一条(成对的)铜线带,我们将在铜带上映射实现makeCoperWire的汇编器

您可能会认为皮带与元素列表或无限流相对应,这取决于您使用皮带的方式。

通常,如果类型构造函数F支持函数在其内容上的映射,则将其称为函子:

铀矿石加工很有趣。它是在离心机中完成的,该离心机接受铀矿石并产生铀的两个同位素。

这里的新事物是输出是概率性的。在大多数情况下(平均占99.3%的时间),您会得到铀238,只有偶尔(占时间的0.7%)会得到铀235(发光的)。此处,加号用于实际编码求和类型。在Haskell中,我们将使用Either类型构造函数,该构造函数将生成sum类型:

离心机输出类型中的两个替代方案需要不同的动作:U235可以变成燃料电池,而U238需要后处理。在Haskell中,我们将通过模式匹配来实现。我们将应用一个函数处理U235,将另一个函数处理U238。在Factorio中,这是使用过滤器插入程序(也称为紫色插入程序)完成的。过滤器插入器对应于一个选择替代项的函数,例如:

Maybe数据类型(或某些语言中为Optional)用于解决发生故障的可能性:如果联合包含U238,则无法获得U235。

每个过滤器插入器都针对特定类型进行了编程。在下面,您可以看到两个紫色插入器,它们用于将离心机的输出分成两个不同的箱:

附带地,混合传送带可以被视为携带求和类型。皮带上的物品可以是例如铜线或钢板,可以写为CopperWire SteelPlate。您甚至不需要使用紫色插入器将它们分开,因为任何插入器在连接到汇编器的输入端时都具有选择性。对于给定的汇编器,它将仅拾取配方输入的项目。

每个传送带都有两个侧面,因此很自然地用它来运输成对的皮带。特别是,可以将一对皮带合并为一对皮带。

我们不使用汇编程序来执行此操作,仅使用某些皮带机制,但是我们仍然可以将其视为功能。在这种情况下,我们将其写为

由于我们可以将皮带合并应用于任何类型,因此我们可以将其编写为多态函数

mergeBelts ::(皮带a,皮带b)->皮带(a,b) mergeBelts(MakeBelt as,MakeBelt bs)= MakeBelt(zip as bs)

(在Haskell模型中,我们必须将两个列表压缩在一起以获得对的列表。)

贝尔特是个函子。通常,具有这种合并能力的函子称为单面函子,因为它保留了类别的单面结构。此处,乘积因子类别的单项式结构由乘积(配对)给出。任何单向子仿函数F必须保存该产品:

单面体结构还有一个方面:单位。该设备与任何东西配对时,不会对其执行任何操作。更准确地说,对(Unit,a)在所有意图和目的上均等同于a。理解Factorio中单位的最好方法是问一个问题:什么皮带,当与a的皮带合并时,会产生a的皮带?答案是:一无所有。将一条空皮带与其他皮带合并在一起没有任何区别。

合并两个皮带的能力以及创建空皮带的能力使Belt成为单向仿函数。通常,除保存产品外,函子F呈单向性的条件是产生能力

至少在Factorio中,大多数函子不是单项的。例如,箱子不能存储货币对。

如前所述,大多数汇编器配方都采用多个参数,我们将其建模为元组(产品)。我们还讨论了部分应用程序,该应用程序本质上需要一个装配器和一种成分,然后生产一种“预涂”装配器,其配方所需的成分更少。现在想象一下,您有一条由单一成分组成的整条皮带,并在其上绘制了一个汇编程序。在当前的Factorio中,该汇编器将接受一项,然后卡住以等待其余项。但是在我们称为Functorio的扩展版本的Factorio中,将多输入汇编器映射到单一成分的传送带上应该会产生“预备”组装器的传送带。例如,红色的科学汇编程序具有签名

将其映射到CopperPlate皮带上时,应产生一部分应用的装配工皮带,每个都有以下配方:

现在,假设您已经准备好齿轮。您应该能够产生出红色的科学带。如果只有一种方法可以将第一条皮带套在第二条皮带上。像这样:

在这里,我们有许多准备好的组装工,还有一些齿轮,其输出是红色的科学带。

支持这种合并的函子称为应用函子。皮带是一种实用的函子。实际上,我们可以说它具有适用性,因为我们已经确定它是单项式的。的确,单义性使我们可以合并两条腰带以得到一对腰带

我们知道,有一种方法可以将Gear-> RedScience汇编程序应用于产生RedScience的Gear。这就是汇编程序的工作方式。但是出于此参数的目的,让我们为该应用程序指定一个明确的名称:eval。

(gtor gr只是将功能gtor应用于参数gr的Haskell语法)。我们正在抽象可应用于项目的汇编程序的基本属性。

现在,由于Belt是一个函子,我们可以将eval映射到成对的皮带上,并获得RedScience的皮带。

apBelt ::(Belt(齿轮-> RedScience),皮带齿轮)->地带红色科学 apBelt(gtors,gear)= mapBelt eval(mergeBelts(gtors,gears))

回到我们最初的问题:给定铜板带和齿轮带,这就是我们产生红色科学带的方式:

redScienceFromBelts ::(皮带铜板,皮带齿轮)->地带红色科学 redScienceFromBelts(beltCu,beltGear)= apBelt(mapBelt(curry makeRedScience)beltCu,beltGear)

我们使用两个参数的函数makeRedScience并将其映射到铜板带上。我们得到了许多优质的汇编程序。然后,我们使用apBelt将这些装配器应用于齿轮传动带。

要获得适用函子的一般定义,只需用通用函子F代替Belt,用a代替CopperPlate,用b代替Gear就足够了。如果存在多态函数,则函子F适用:

为了使图更完整,我们还需要等分单位律。一个叫做pure的函数扮演着这个角色:

这只是告诉您,有一种方法可以创建带有单个项目的皮带。

在Factorio中,函子的嵌套受到极大的限制。可以生产皮带,也可以将它们放在皮带上,这样您就可以拥有许多皮带,皮带腰带。同样,您可以将箱子存放在箱子中。但是您不能有载重皮带。您不能选择装满铜板的皮带并将其放在另一皮带上。换句话说,您不能运输一大堆东西。实际上,在现实世界中这没有多大意义,但是在Functorio中,这正是我们实现monad所需要的。想象一下,您有一条皮带,上面有一束皮带,这些皮带上都装有铜板。如果皮带是单子带,您可以将整个东西变成一条铜板皮带。此功能称为联接(在某些语言中为“展平”):

此功能仅收集所有皮带上的所有铜板并将它们放在一条皮带上。您可以将所有子带合并为一个。

同样,如果箱子是单子的(没有理由不应该这样),我们将有:

一个monad还必须支持可应用的纯正(在Haskell中,它称为return),并且实际上,每个monad都自动适用。

Factorio的许多其他方面导致了编程中有趣的话题。例如,火车系统需要处理并发性。如果两列火车试图进入同一路口,我们将进行一次数据竞赛,在Functorio中,这称为火车撞车事故。在编程中,我们避免使用锁进行数据争用。在Factorio中,它们称为火车信号。而且,当然,锁会导致死锁,这在Factorio中很难调试。

在函数式编程中,我们可能使用STM(软件事务存储)来处理并发。接近交叉口的火车将开始交叉口交易。它会暂时不理会所有其他火车,并乐于穿越。然后它将尝试实施交叉。然后,系统将检查与此同时是否有另一列火车成功通过了相同的过境点。如果是这样,它将说“哎呀!再试一次!”。

这是一篇不错的文章,内容超赞。我以为在第一段中有一个有趣的句子,您提到真实的工程是功能性的,然后使用一个在虚拟世界中的游戏集来说明这一点。

......