Skip to content

Latest commit

 

History

History
2139 lines (1224 loc) · 111 KB

File metadata and controls

2139 lines (1224 loc) · 111 KB

十一、应用范畴论

在幼儿园,我们学会了如何阅读时间。在高等数学中,我们学习了如何抽象出一个 12 小时的时钟,并称之为单子。

在小学,我们学习了几何、逻辑推理和函数。

高中时,我们学习了代数、线性方程和二次方程。我们忙着在问题的细节中进行深入的讨论,以至于我们看不到任何一点用处。

在此处查看 K-12 教育中使用的学习材料:https://www.ixl.com/math/kindergarten/match-analog-clocks-and-times

快进到我们的日常工作。为了显得聪明,我们经常会问,它会扩展吗?无论是什么都是。

想知道阅读时间和水平缩放是如何相关的吗?他们是,深深地。我们将在本章中了解如何进行。

本章的目标是完成以下工作:

  • 获得对范畴论的工作理解
  • 欣赏范畴论、逻辑和类型理论之间的深刻联系
  • 理解在 lambda 表达式上下文中绑定、转换和应用程序的含义
  • 了解同态的不同类别以及如何使用它们
  • 从范畴论学习运用写作技巧
  • 了解什么是接口驱动开发
  • 请参见知识驱动系统的价值
  • 应用我们对范畴论的理解来构建更好的应用程序

我们的目标

到本章结束时,我们将看到我们在学校上的数学课的价值。我们将了解在水平扩展我们的软件解决方案时,我们在高中数学课上学到的东西是如何应用的。

下图暗示范畴论、函数式编程和逻辑是等价的:

嗯?

我认为范畴论是关于一组物体和连接它们的箭头,而证明理论是关于用逻辑证明某些东西。我们都知道函数编程是关于软件的。这三件事怎么可能有关联呢?

这似乎和我们在学校上的数学课一样有用,对吧?

你的悲观是可以理解的。请以开放的态度继续,并保持就座。数学、逻辑和计算。它们只是解决相同问题的三种不同方法。

范畴论、证明理论和函数式编程怎么可能是一回事?(为什么要在意?)

“科学家们从解决难题中获得满足感。这是关于探索,而不是圣杯。”

-艾萨克·阿西莫夫

分解

让我们将每个部分分解,以牢牢把握问题的广度:

如果为什么是构建应用程序的动机,那么如何描述我们的应用程序如何更好,以及什么是我们的最终产品/应用程序。

如何与作为人类的我们如何推理有关。这是范畴论的领域。

什么与具体内容相关。这是数学和计算的领域。我们将使用代数来帮助定义什么。稍后,我们将看到我们在代数方面的工作可以直接转移到函数式编程。

代数与未知

代数是数学的一个分支,与算术非常相似。它使用数学使用的四种主要运算:加法、减法、乘法和除法(+、-、/、)。代数还引入了一个新元素:未知。在数学中,未知数在等式的右边。记住像2+3这样的数学题。在对操作数(2 和 3)执行数学运算之前,答案是未知的。在代数中,我们用符号代替未知占位符。一个代数方程是2+3=x*。这是一个代数方程,表示等号两边相等。操作数 2 和 3 是已知的,x是未知的。

代数的目标是通过确定未知符号的值来求解方程:

还记得我们的数学老师接下来会做什么吗?

他们会交换符号和数字,让问题更难解决!然后,他们会给我们越来越复杂的方程,像这样:

他们迫使我们执行多个步骤来简化我们的问题。由于双方必须保持平等,我们可以使用重量平衡来设想问题:

我们是如何解决更复杂的问题的?答:将其切成更容易处理的小块,如下图所示:

代数与现实世界没有什么不同,它依靠规则使事物正常工作。以下是一些规则:

  • **规则 1:**代数方程中的变量x不能同时代表同一方程中的两个不同值

例如,如果我们有一个等式,x+x=6,那么下面是真的:1+5=6;但是,由于x不能代表同一等式中的两个不同值,因此对x有效的唯一值是 3(使用 1 和 5 表示 x 将违反规则 1

  • **规则 2:**如果我们想要两个变量代表两个不同的值,我们必须使用两个不同的符号。例如,x+y=6。
  • **规则 3:**当同一个变量符号在同一个等式中多次使用时,表示相同的值。
  • **规则 4:**默认操作为乘法。2*x 与 2x 相同。所以,如果没有运算符,我们可以假设我们正在处理默认的运算,乘法。
  • **规则 5:*括号可用于对术语进行分组。如果我们看到 3(2),这与 3(2)相同,这与 3*2 相同。所有三组术语都等于 6(而不是 32)。

我们现在的工作是将这个问题分解成更小的步骤,并计算出x的值。(提示:您以前见过。)

  • **规则 6:**不同的符号可以在同一个等式中表示相同的值,但它们不一定要表示相同的值。

正如我们看到的,xy具有相同的值,但仅在第二个 if 语句中。随着x值的变化(从 0 到 1 到 2),y 值也随之变化(从210。这就是为什么符号xy被称为变量的主要原因。它们可以变化。

在基于图灵的语言中处理变量的方式与 Lambda 演算(纯函数编程)语言截然不同。

在基于图灵的语言(如 C)中,变量x的值存储在运行 C 程序的计算机内存中的特定位置。它可以是一个全局变量,这意味着其他运行过程可以访问和更改(akamutate)它的值:

在像 Haskell 这样的纯函数式语言中,永远不会存储值。可以创建新的,并沿着执行链传递。

代数的现实应用

有没有想过这些方程式有什么用?

在尝试在现实世界中建模时,它们可能很有用。让我们拿一些代数方程,画出它们的解。绘制一个方程就像使用方程的结果(函数的输出)来绘制线条和曲线,这些线条和曲线可以用来说明和/或预测现实生活中的事情。

线性方程与需求定律

线性方程组可用于描述具有直线斜率的事物:

需求定律表明,随着产品价格的上涨,对该产品的需求将减少。这是因为人们自然避免购买会迫使他们放弃购买他们更看重的东西的产品。该图表明需求曲线呈下降趋势。价格越低,销售的产品就越多。

建筑设计师使用线性方程来确定屋顶线的坡度,谷歌地图使用线性方程来告诉您旅行需要多长时间。

我们对线性方程函数f(x)=3x+2了解多少?

对于每个输入x,我们得到一个且只有一个结果。这就是为什么如果我们输入每一个可能的数字(作为值x),我们会得到一行!这就是为什么垂直线在几何学中很难实现。

我们周围的二次方程

下列方程称为线性方程:

y=x+2

这是因为所有变量都是一的幂。

考虑到-42x值,我们可以很容易地计算出y值,如下所示:

如果我们输入所有可能的x值(包括小数点为 0.1、0.11、0.12 等的值),我们会得到一条直线。可以说,是所有可能的x值的集合,范围是所有可能的y值的集合。请注意,任何非垂直或非水平线都是一个函数,其域和范围由所有实数组成。

很容易看出,我们前面的*f(x)*函数只是从一组数字到另一组数字的映射。

当我们使用 2 或更大的指数时,方程称为二次方程。下面是一个例子:

y=x2+1

具有线性和二次函数的函数合成

让我们用我们的g(x)=x+2线性方程表来组成我们的f(x)=x2+1二次方程。这里有一种方法可以组合我们的两个函数:y=f(g(x)】。我们会说y等于f-compose-g of xy=f o g,其中 o 是我们的复合运算符。其工作方式是,我们为x赋值,然后将该值插入g,计算 g(x),然后将结果插入f

我们将1输入g并表示为g(1)。我们输入g(1)到f得到f(g(1)】

让我们将g(1)替换为从1映射到g(1)的值,即3

g(1)替换为3,得到如下结果:

当我们将3输入g时,我们评估x2+132+1表达式,等于10

所以,f(g(1)】等于10

如果我们反转我们的函数嵌套,比如g(f(1)),会怎么样?我们会得到同样的答案吗?

f(1)=x2+1=1+1=2 g(2)=4

我们从前面的线性方程表中得到了f(2)=4

由于g(f(1))=10f(g(1))=4,我们知道以不同的顺序组合相同的函数可能会得到不同的结果。

我们还看到,在编写时,我们要么用表中相应的/映射的值替换函数/值,要么对函数表达式求值并替换为该值。我们已经看到函数的引用完整性特性如何允许我们缓存其值。所以,在函数第一次被求值之后,我们所做的就是在编写函数时进行一系列的值替换。

“如果 A 等于成功,那么公式是 A 等于 X 加上 Y 和 Z,X 表示工作,Y 表示玩耍,Z 表示闭嘴。”

-爱因斯坦

二次方程的更多示例

下列各项都是二次的吗?

在线绘制您自己的方程式:https://www.desmos.com/calculator

黄金比例

让我们看一个更有趣的二次方程。希腊人认为矩形的比例最为美观,它是大小矩形比例相同的形状。

这被称为黄金矩形x2+x=1的解为x=1.61803398875,我们将其缩短为x=1.61。

希腊人并不是唯一认为黄金比例完美的人。

当我们仔细观察时,我们将看到商业中的黄金比例:

还记得吗?第一章中的斐波那契序列及其与递归的关系,Go中的纯函数编程?0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89。在这个序列中,每个项都是前两项的总和。如果我们仔细观察,我们会在自然界中看到这个序列。例如,百合花有 3 片花瓣,毛茛 5 片,金盏花 13 片,紫苑 21 片。大多数雏菊有 34、55 或 89 片花瓣。

向日葵头部的种子从其中心放射出两个相互交错的螺旋线,一个顺时针旋转,另一个逆时针旋转。通常有 34 条螺旋线顺时针旋转,55 条反方向旋转。

我们对数学、编程、艺术和科学与自然之间的关系了解得越多,我们就越能找到在我们周围工作的大师建筑师之手的证据。

我们越了解我们周围的系统是如何工作的,我们看到的模式就越多。稍后,当我们更仔细地研究范畴论时,我们将研究分解(将问题分解成小的、可理解的片段)和组合(将这些片段重新组合在一起)的重要模式。FP 允许我们将单片应用程序分解为一组乐高积木,如果需要,可以针对不同的系统以不同的配置进行组装,并且我们可以以易于理解的声明方式进行组装。

考虑到不变性和引用透明性的保证,操作发生的时间就不那么重要了。这简化了编码并发解决方案的组合复杂性。这也允许通过使用并行来无害地提高性能,并且在时间甚至没有完全定义的分布式系统中,这是值得的。

代数基本定律

学习代数的这些基本定律。我们很快就会再见到他们!

稍后,您将了解函数组合具有以下特性:

  • 它是关联的
  • 它通常是不可交换的
  • 通过分配(g+h)∘ f=g∘ f+h∘ f(g+h)∘ f=g∘ f+h∘ f
  • 它通常不是通过f 分配的∘ (g+h)=f∘ g+f∘ h

数学中的对应关系

范畴论尽可能抽象地呈现数学,并去除所有非本质属性,为所有数学提供了一个框架。

还记得你的数学课吗?以下是一些课程:

| 数学分支 | 描述 | | 代数 | 代数用法则描述其元素之间的关系,例如,结合性、交换性。有不同类型的代数,如线性代数、李代数、交换代数和抽象代数。在代数中,我们经常在方程式中用字母代替数字。例如1+2=3形式变为x+y=z。布尔代数是另一种类型的代数,其中变量是真值(真和假)而不是数字。 | | 几何学 | 几何学研究空间中形状和位置的性质。它提供了确定圆的周长(c=2πr等)和确定各种形状面积的公式。 | | 思维方式 | 逻辑提供数学推理的规则。布尔代数是数学逻辑的一种形式。 | | 数值分析 | 数值分析为数学问题的近似解提供了算法。它通常使用计算能力快速接近可能无法手动解决的真正解决方案。 | | 微积分 | 微积分是分析中证明的结果的应用。 |

数学是对数据结构的研究:形状、数字、组、集合等等。我们研究它们的结构、行为以及它们如何相互作用。

柯里、霍华德和兰贝克发现数学的所有分支都是完全相同的东西!他们意识到,在某种抽象层次上,所有数学理论的结构都是相同的。我们可以将逻辑的结构变形为范畴类,我们可以将这种结构转化为类型理论。宇宙中的所有态射,以及由此产生的所有活动,都可以用范畴论来描述。

例如,当我们考虑一个电磁场中的光子粒子,一个飞行中的足球,和一个弹跳的 C(音符),它们在我们提供上下文之前似乎没有太多的共同点。从波动理论的观点来看,它们都是相同的问题。现在,改变环境或向心力;同样,它们都是相同的问题,只是在不同的背景下。当我们把所有不必要的细节都抽象掉时,剩下的就是数学结构。

以这种方式使用抽象的好处是,我们开始看到以前从视图中隐藏的事物之间的联系。我们何时可以创建和使用工具,使我们能够以不同的方式将问题集上下文化。我们拥有范畴论的全部力量来启发我们的道路。了解这些概念的软件工程师能够更好地执行数据分析。学习应用功能编程概念的软件工程师可以构建更可靠的解决方案,这些解决方案可以跨多个核心和云本地集群中的多个计算实例进行水平扩展。不难看出关于函数式编程的大惊小怪是怎么回事,对吧?

证明理论

证明理论是数学的一个分支,我们在其中做出假设并应用逻辑来证明某事。例如,如果 a 和 b 可以被证明是真的,那么 a 是真的,b 也是真的。

逻辑连接词

下表按优先顺序描述了逻辑连接词:

| 符号 | 数学名称 | 英文名称 | Go 操作员 | 示例 | 意思是 | | ¬ | 反面 | 不 | ! | ——a | 不是 | | ∧ | 结合 | 和 | && | A.∧ B | a 和 b | | ⊕ | 排他析取 | 异或 | NA | A.⊕ B | a 或 b(但不是两者都有) | | ∨ | 分离 | 或 | || | A.∨ B | a 或 b | | ∀ | 通用量化 | ∀ x:A(x)表示 A(x)对所有 x 都是真的 | NA | ∀a:a | a 型的所有值 | | ∃ | 存在量化 | ∃ x:A(x)表示至少有一个 x 使得 A(x)为真 | NA | ∃a:a | 存在 a 类型的某个值 | | ⇒ | 实质含义 | 暗示 | NA | A.⇒ B | 如果是 a,那么是 b | | ⇔ | 材料等效 | A.⇔ 只有当 a 和 b 都为假,或者 a 和 b 都为真时,b 才为真 | NA | A.⇔ B | a 当且仅当 b | | | 定义为 | A.≡ b 表示 a 被定义为 b 的另一个名称 | NA | A.≡ B | a 在逻辑上等同于 b | | ⊢ | 旋转栅门 | A.⊢ b 意味着 a 可以从 b 证明 | NA | A.⊢ B | a 可以从 b 证明 |

NA=不适用,即 Go 中没有此符号。

还有其他的逻辑符号,但这些是一些更重要的符号。

在软件中,我们通过组合这些符号和其他术语(如变量)来使用逻辑来证明某件事是否属实。

以下是使用量化符号的示例:

f: A ⇒ B∀a:A∃b:B使得b = f(b)

换句话说,有一个从aB 的函数,其中对于a 类型的所有值 a,存在B类型的一些值 B,使得B=f(a)

逻辑不一致

以下函数签名表示逻辑不一致的函数:

def factorial(i int) int

问题是没有为负整数定义阶乘。

部分函数

如果我们的函数没有为域中的所有值定义/一致,则称之为部分函数(与总函数相反)。如果我们的函数不一致,那么我们就有可能在运行时遇到意外错误。

解决此问题的主要方法有两种:

  • 我们可以通过将域的大小减少到只有正整数来解决这种不一致性
  • 我们可以使用失败单子(如验证或析取)来捕获出错的情况

真值表

真值表包含对命题的解释。解释是对命题价值的计算:

| A. | B | ——a | -b | A.∧ B | A.∨ B | A.⊕ B | A.→ B | A.↔ B | | T | T | F | F | T | T | F | T | T | | T | F | F | T | F | T | T | F | F | | F | T | T | F | F | T | F | T | F | | F | F | T | T | F | F | F | T | T |

“正确陈述的对立面是错误的陈述。深刻真理的对立面很可能是另一个深刻真理。”

-尼尔斯玻尔

条件命题

以下命题说明了同样的事情:

  • 如果是 a,那么是 b
  • a 表示 b
  • a→ b
  • a⇒ b

变量a为假设,b为结论。结论总是正确的,除非a是正确的,而b是错误的。对此的一种思考方式是:“如果猪能飞,那么……”在这样一个明显错误的陈述之后,你得出的任何结论都是正确的。如果ab都是真的,那么显然从 a 到 b 都是真的。但是,如果a为真,而b为假,那么当从ab时,我们将得到一个假值。

逻辑等价

现在,我们可以使用真值表来确定复合命题的结果。自年起∨ ba→ b具有相同的真值,它们被认为在逻辑上是等价的,我们用a 表示∨ B≡ A.→ b方程。

| A. | B | ——a | ——a∨ B | A.→ B | | T | T | F | T | T | | T | F | F | F | F | | F | T | T | T | T | | F | F | T | T | T |

逻辑上等价的陈述可以是:“如果珍妮坐在她的办公桌旁,那么她就在家。”这是一个逻辑陈述。一个合乎逻辑的说法可能是:“如果珍妮不在家,那么她就不会坐在办公桌上。”

我们通过创建一个假设及其结论来创建逻辑等价。前面的假设是:“如果 Jenny 坐在她的办公桌旁,结论是:她会在家。我们确定每个人的真实性,并比较他们的真实性(真或假)。

条件命题的逆命题

让我们用真值表来证明(a → b)∧(b → a) ≡ a ↔ b方程:

| A. | B | A.→ B | B→ A. | (一)→ (b)∧(b)→ (a) | A.↔ B | | T | T | T | T | T | T | | T | F | F | T | F | F | | F | T | T | T | F | F | | F | F | T | T | T | T |

换句话说,一个双条件命题*(a↔ b)* 相当于条件命题的连词(a→ b) 及其逆*(b)→ a)* 。

秩序问题

记住这句话:“如果珍妮坐在她的办公桌旁,那么她就在家了?”

相反的说法是,“如果珍妮在家,那么她就会坐在办公桌前。”相反的说法是通过交换假设和结论而产生的。反之如何改变句子的逻辑?(*詹妮可能在家,但不在办公桌上吗?*相同的单词以不同的顺序可以改变结果的真值。

类似地,条件的倒数也可以改变逻辑。例如,如果詹妮坐在她的办公桌上,她就会在家里,然后她会在家里,如果詹妮不坐在她的办公桌上,那么她就不会在家里了。(詹妮可以在家里,而不是在她的办公桌上吗?)

看看我们如何使用真值表来组合语句并确定其结果真值?

Curry-Howard 同构

Curry-Howard 同构说类型是命题,程序是它们的证明。命题是一种断言(声明性语句),可以是真的,也可以是假的(但不能同时是真的和假的)。

命题示例

请考虑下列命题:

  • 方程 2*3=5
  • 如果外面有暴风雨,我就带着优步去上课;否则,我走路,如果天气晴朗,我就骑自行车:

| 变量 | 条 | | A. | 外面正在下暴雨 | | B | 我带优步去上课 | | C | 我走路 | | D | 天气晴朗 | | E | 我骑自行车 |

以下为书面逻辑版本

a 意味着 b 和((不是 a)意味着(c 和(d 意味着 e))

以下为逻辑符号版本

(a⇒ (b)∧ (a)⇒ (c)∧ (d)⇒ e) )

不是命题

以下是 not 命题的示例:

  • x=5(这不是对真理的断言,这是一项任务)
  • x+y=5(没有足够的信息作为断言,答案取决于缺少的数据)

命题可以使用连接词(and 或 not)组合术语。

λ演算

阿隆佐·丘奇(Alonzo Church)将形式逻辑(称为非类型 Lambda 演算)引入计算机科学,包括替换、抽象和应用。让我们记住这些术语,并在本章后面的 Go 中实现 lambda 表达式时使用它们。

为什么这么正式?

为什么我们关心坚持逻辑(和代数)方程的形式主义和规则?

“具有讽刺意味的是,形式主义的约束解放了我们,使我们成为最好的人。”

礼仪的重要性

尊重长辈,说“是的,女士”和“是的先生”不仅仅是社会形式主义。这被称为遵循协议的*。它帮助我们以一致的方式进行沟通。遵循形式主义有助于我们采取适当的行动。实践公民美德的例子包括:*

  • 诚实
  • 保持自我控制
  • 善待人类同胞

当我们以身作则,以透明和善良的方式互相服务,并防范贪婪和其他形式的邪恶时,我们可以以不太可能冒犯他人的方式自由地与他人交往。

领导人的攻击性语言和不当行为如何影响我们社会的力量?公然不尊重执法人员有什么好处?

当每个人都明白礼貌和谨慎慷慨的重要性时,我们就生活在一个强大的社会体系中,在这个体系中,我们都有机会茁壮成长。当我们将这个概念转移到我们的软件开发工作中时,我们最终会得到更好的解决方案。我们实现这个系统的方式是通过逻辑。如果我们的系统在逻辑上是健全的,那么它们将可靠地帮助我们实现我们的目标。

函数式编程中的历史事件

函数式编程的历史非常迷人。函数式编程语言是基于一个优雅而简单的数学基础,lambda 演算。

“要理解一门科学,就必须了解它的历史。” -奥古斯特·孔德

让我们看看导致 Lambda 演算的发现。

乔治·布尔(1815-1864)

逻辑起源于古希腊,如亚里士多德和欧几里得。在布尔之前,逻辑学是希腊语;它是以语言的形式表达的。布尔是第一个将逻辑转换成代数符号的人:

  • 真=1
  • 假=0
  • 和=产品(AxB)
  • 或=总和(A+B)

奥古斯都·德·摩根(1806-1871)

德摩根定律指出,所有逻辑运算都可以用表示,而则不能。此外,所有逻辑运算也可以仅用而不是表示,或者仅用而不是表示:

a∧ b=,(,-a)∨ (b) a∨ b=,(,-a)∧ (b)

第一个等式表示ab均为真,当且仅当ab中至少有一个为假。第二个等式表示,ab中至少有一个为真,当且仅当 a 和 b 都为假时。

弗里德里希·路德维希·戈特洛布·弗雷格(1848-1925)

弗雷格是德国数学家,被许多人认为是分析哲学之父。他研究逻辑中函数的使用,是第一个使用 curry 的人。弗雷格发明了公理谓词逻辑。公理是我们认为正确的陈述/命题;它们是如此不言而喻,以至于没有任何其他理由能让它变得更清楚。这些都是简单的事实。

假肢

命题逻辑的以下规则称为方式

  1. 如果树还在电线上,我们就没有电了
  2. 这棵树还在电线上
  3. 我们没有权力

查尔斯·卢特威奇·多奇森(1832-1898)

查尔斯·多德森(笔名:刘易斯·卡罗尔)写了几本书,在书中,他通过操纵逻辑去除了直觉和任何先入之见,即使它看起来像胡说八道。让我们看看他小说《爱丽丝梦游仙境》中的文学废话。多德森经常无视常识,通过操纵语言创造一个全新的世界。这个故事在理智和胡说八道之间保持了平衡,保持了逻辑性,尽管它有时看起来完全不合逻辑。例如,当 Alice 在Looking Glass Land的前后世界中移动时,她发现了一本用看似难以理解的语言写成的书:

那是布瑞利,还有那件紧身衣

在 wabe 中旋转和 gimble:所有的 mimsy 都是 borogoves,

而这一时刻却被超越了。

“当心那些胡言乱语,我的儿子!

咬人的下颚,抓人的爪子!小心朱布鸟,避开它

弗鲁米奥斯·班德斯纳奇!"

中,爱丽丝试图通过镜子跟上红皇后;尽管她一直在跑步,但她仍然在原地不动。爱丽丝说:

嗯,在我们国家,爱丽丝说,仍然有点气喘吁吁,如果你像我们一直做的那样跑得很快,你通常会跑到别的地方。

红女王的比赛经常被用来说明一些深刻的概念,例如:

  • 时间旅行
  • 膨胀的可观测宇宙边缘附近星系的相对论效应
  • 我们在 IT 行业采用新技术以跟上竞争对手的努力(尽管几年后,回首往事,我们意识到我们实际上并没有改进我们的系统,我们只是改变了技术,有时对我们不利)

稍后我们将看到一个名为幻想之地的 FP 库,它的名字很可能来自《爱丽丝梦游仙境》等作品中的无意义逻辑。

阿尔弗雷德·怀特海和伯特兰·罗素(1903)

在罗素写给弗雷格的一封信中,提出的“理发师悖论”发现弗雷格的逻辑存在问题:

考虑到一个城镇唯一的理发师为每个人剃须,除了那些自己剃须的人。我们可以推断出两件事:

  • 如果一个人不给自己刮脸,理发师就会给他刮脸
  • 如果有人自己刮胡子,理发师就不会

矛盾的是:理发师不能刮胡子

第一种说法是,如果理发师不自己刮胡子,那么理发师就会自己刮。然而,第二种说法与第一种说法直接矛盾。

罗素和怀特海合作证明/解决了巴伯悖论,并证明数学是一个形式化的逻辑框架。在 1912,他们傲慢地创作了一个题为 MathematicMathematica 的作品(这是艾萨克·牛顿用来命名他的作品的同名,包括运动定律,形成经典力学的基础,万有引力定律,以及开普勒行星运动定律的推导)。

Russell 和 Whitehead 的工作被证明是不切实际的(其中包括一份 450 页的证明,证明1+1=2。具有讽刺意味的是,逻辑替代在他们的《数学原理》中并没有正式化。(当我们研究 Lambda 演算时,我们将了解逻辑替换的含义。)

摩西·申芬克尔(1889-1942)

Schonfinkel 是 1924 年左右发明组合逻辑的俄罗斯数学家。组合子是一种高阶函数,它仅使用函数应用程序和早期定义的组合子来定义其参数的结果。这种替换技术将多个函数参数简化为一个参数,后来在 Haskell Curry 之后被称为 Curry。

下表说明了 Schonfinkel 组合器:

| 定义 | 首字母缩略词-德语 | 功能类型 | | λx。x | I-识别功能 | 身份 | | λx,y。x | K-Konstanzfunktion | 常数 | | λx,y,z。xz(yz) | S-Verschmelzungsfunktion | 合并 | | λx,y,z。xzy | T-Vertauschungsfunktion | 交换 | | λx,y,z。x(yz) | Z-Zusammensetzungsfunktion | 作文 |

哈斯克尔咖喱-1927

Haskell Curry 在 1927 年引入了组合逻辑,消除了使用变化的变量。它是基于组合的。组合子是一种高阶函数,它使用函数应用程序和先前定义的组合子从其参数生成结果。Alonzo Church 后来设计了一种类似的形式主义,称为的****Lambda 演算,其中 Lambda 表达式表示函数抽象,由一组有限的组合词代替。有关详细信息,请参见本章后面的Lambda 演算部分。

格哈德·根岑(1936)

1936 年,一位名叫 Gerhard Gentzen 的德国数学家用原始递归算法证明了一阶算术(加法和乘法)是一致的。Gentzen 使用序贯演算,这是一种条件重言式(一系列真语句),根据推理规则和过程构建参数(https://en.wikipedia.org/wiki/Inference )具有零个或多个断言。请注意,序列演算与自然演绎非常相似,自然演绎由一个或多个断言组成。

阿隆佐教堂(1930 年、1940 年)

丘奇阅读了《数学原理》,并决定改进它。Church 使用函数抽象将形式数学逻辑应用于计算机科学,并使用变量绑定和替换进行应用。1930 年,Church 发布了 Lambda 演算的第一个版本,它构成了他所谓的有效可计算函数的基础。

1935 年,克莱恩和罗瑟证明了这在逻辑上是不一致的。Church 很快做出了回应,推出了一个名为简单类型 Lambda 演算的改进版本,该版本通过定义高阶逻辑语法但不包含递归函数的类型化系统修复了非终止程序的问题。后来,在 1940 年,丘奇发明了 Lambda 演算,它只由函数组成,而不涉及字符串和数字等具体值。它只适用于函数。

函数可以接受函数和返回函数。Haskell Cury 打算将λ演算作为数学的基础。您需要某种形式的递归类型才能在其中编写任何有趣的程序。Haskell Curry 的组合逻辑工作是函数式程序设计语言的基础。

艾伦·图灵(1950)

大约在阿隆佐·丘奇(Alonzo Church)出版他的兰姆达演算(Lambda 演算)的同时,艾伦·图灵(Alan Turing)介绍了图灵机器,它可以执行任何计算任务,也就是兰姆达演算(Lambda 演算)可以计算的任何东西。图灵完整性是一种抽象的能力陈述,而不是用于实现该能力的特定语言特性的规定。用于实现图灵完整性的特性可能会大不相同;Fortran 系统将使用循环构造,甚至可能使用goto语句来实现重复。像 Haskell 和 Prolog 这样的纯函数式语言使用递归。

麦克莱恩和艾伦伯格(1945)

桑德斯·麦克莱恩(左)和塞缪尔·艾伦伯格(右)在 1945 年发表了题为《自然等价的一般理论》的论文,介绍了范畴、函子和自然变换的概念。在他们对代数拓扑的研究中,他们给出了对象、贴图和贴图组合的明确定义,目的是理解保持数学结构的过程。

约翰·麦卡锡(1950)

接下来是约翰·麦卡锡,他发表了论文符号表达式的递归函数及其机器计算,第一部分http://dl.acm.org/citation.cfm?id=367199 )。1958 年,他的一个学生根据麦卡锡的教学编写了一个解释器,该解释器成为一种基于纯数学的编程语言,称为 Lisp。Lisp 是第一种函数式语言。最早流行的基于类型系统的计算机语言是 Fortran 和 Cobol,出现于 20 世纪 50 年代。

柯里·霍华德·兰贝克通信(1969)

Curry、Howard 和 LambekCHL)发现了范畴论中的对象、逻辑中的命题和编程语言中的类型之间的一一对应关系。

CHL 研究了自然演绎规则和类型化 Lambda 演算的规则类型,发现它们是相同的。

如果我们去掉上表中的红色项,它们是相同的。因此,Church 的 lambda 类型与 Gentzen 的逻辑公式一一对应。类型检查与校对相同。

  • 逻辑包括蕴涵结构
  • 编程有数据记录和函数构造
  • 范畴论中的箭头是函数(也可以是数据)

作为它们如何联系的一个例子,认为逻辑中的命题可以是真的或假的。类似地,类型可以有人居住,也可以没有人居住。真正的命题是有人居住的。void 类型为 false。如果我们能产生一个类型的元素,那么我们已经证明了我们的命题。

CHL 意识到笛卡尔闭范畴、直觉命题逻辑和简单类型的 Lambda 演算本质上都是一样的。

让我们看一下对应表:

| 范畴论 | 逻辑理论 | 类型理论/Lambda 演算 | | 物体 | 主张 | 类型 | | 态射 | 校样 | 功能 | | 态射之间的等价性 | 证明之间的等价性 | Lambda 演算项之间的 Beta-eta 等价 |

这三个研究领域从不同的角度独立得出了相同的发现,但它们在每种情况下描述的数学结构基本相同。

罗杰·戈德曼(1958)

1958 年,罗杰·戈德曼(Roger Godement)写了一本关于层理论的书,首次引入了单子的概念。滑轮是捕捉流形局部数据的对象,但这样做可以让人们看到空间作为一个整体的全局属性。流形是什么?它是一个几何物体,例如地球。从你站立或行走的地方看,这一切似乎永远都在继续。然而,如果你绕地球走足够多的时间,你就会意识到它是一个球体。戈德曼所谓的标准结构后来被桑德斯麦克巷(Saunders Mac Lane)称为单子,这个名字一直沿用至今。

莫吉,瓦德勒,琼斯(1991)

1991 年,Eugenio Moggi 撰写了计算概念和 monads,引入了计算范畴语义的概念,以便理解新编程语言的特性。语言通常会添加新的特性来解决特定的问题,但很少以正式的方式仔细指定这些特性。为了理解用这些语言编写的程序,我们需要一个框架来帮助我们理解信息如何在应用程序中流动。

Moggi 描述了一个 C 类和一个具有对象a的自同态函子f

  • AC中的一种类型,其中成员是A类型的值
  • f应用于A并返回另一A

令人惊讶的是,我们用这么少的钱走了这么远。少就是多!

菲利普·瓦德勒、西蒙·佩顿·琼斯和其他人开始使用单子,单子逐渐进入哈斯克尔语。现在,单子是其标准库的一部分。单子的用途包括:

  • 链/链接/连接/组合功能
  • 处理输入
  • 处理副作用
  • 异步/并发处理
  • 登录中
  • 错误处理

奥利维拉·吉本斯(2006)

Gibbons 和 Oliveira 探索了 OOP 迭代器模式的 FP 解决方案。他们使用命令式迭代模式,观察到数据是通过累加逐元素映射的,可以返回相同形状的对象,即元素的转换列表。

他们采用 Kernighan 和 Ritchie 的命令式字数程序(在下面的 C#代码中),并使用遍历运算符和应用函子技术创建了一个替代实现:

public static int[] wc<char> (IEnumerable<char> coll) {
    int nl = 0, nw = 0, nc = 0;
    bool state = false;
    foreach(char c in coll) {
        ++nc;
        if(c == '\n') ++nl;
        if (c == ' ' || c == '\n' || c == '\t') {
            state = false;
        } else if (state == false) {
            state = true;
            ++nw;
        }
    }
    int[] res = {nc, nw, nl};
    return res;
}

下面是 Go 中的一个迭代示例:

鉴于不同的遍历执行不同的功能:

| 功能 | 地图元素 | 创建状态 | 映射依赖于状态 | 依赖于元素的状态 | | collect | X | X | | X | | disperse | X | X | X | | | measure | X | X | | | | traverse | X | X | X | X | | reduce | | X | | X | | reduceConst | | X | | | | map | X | | | |

计算形式如下(K 为计算类型,T 为数据类型):K[T]

以下 FP 计算的性质:

| 计算 | 说明 | 新计算 | 使用计算 | 函子映射 | | Option[T] | 0 | | 1 元素 | 一些(t) | 一些(3) | 更改值 | | List[T] | >=0 个元素 | 列表(t) | 清单(1、2、3) | 更改值 | | Future[T] | 稍后表演 | 未来(t) | 未来(总和) | 改天 | | State[S, T] | 对国家的依赖 | 状态(s=>(s,t)) | 州(s=>(s,s+2)) | 更改 tx | | IO[T] | 外部效应 | IO(t) | IO(putStr(“hi”)) | 修改动作 |

从这个应用程序开始:f(a, b) ==> f(K[a], K[b])用这个指向的f(a:A, b:B):C ==> fk: K[A => B => C]和咖喱:

K[A => B => C] <*>
K[A] <*> K[B]
K[B => C] <*> K[B] == K[C]

然后,在KK(**f**) <*> K**(a**) <*> K(**b)**内的ab****上涂抹f****

我们可以使用应用程序组合来组合具有可遍历性的函数,并表明迭代器上的转换是应用程序的。

这就是大局。有关详细信息,请阅读他们的论文:https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf

简而言之,FP 的历史

20 世纪 30 年代,出现了两种截然不同的解决计算问题的方法。第一个学派聚集在阿隆佐教堂后面。(丘奇在 1929 年左右发展了 Lambda 演算。)丘奇说设计应该是自上而下的,而不是自下而上的。他说,我们应该首先将所有计算视为对数学函数的评估,然后去除抽象,转向机器级操作。通过组合控制复杂性的能力可以说是主要关注点(绝对不是性能)。从这一思路衍生出来的语言包括 ML、Lisp、SmallTalk、Erlang、Clojure 和 Haskell。

另一个计算解决方案来自艾伦·图灵(Alan Turing,丘奇的前学生,1937 年左右开发了图灵机器)。图灵说软件设计应该首先考虑软件运行的硬件。稍后,可以根据需要进行抽象,以实现所需的结果。表现是他们最关心的问题。从这一思想中涌现出来的语言包括 FORTRAN、C、C++、C、Pascal 和 java。

Lambda 演算和图灵机器都是图灵完备的。图灵机基本上是一台通用计算机(具有 if、then、else、分支逻辑和循环结构,如 for 或 while 循环以及读写数据的方法),可以帮助我们解决问题。丘奇证明了图灵机器可以用 Lambda 演算实现。

Lambdas 警告说,基于可能被淘汰的硬件锁定软件设计的脆弱性。直到最近,自下而上的方法才取得了胜利。随着最近多核计算机和分布式处理环境的出现,Lambda 演算正在取得进展。

最近,基于图灵的语言开始采用自上而下的方法。例如,我们开始在 Java7 中看到 FP 特性。越来越多的 FP 功能添加到每个后续版本中。我们也看到 FP 构造被添加到 Python、C++、Cype、PHP 等等。

今天最重要的关注点是什么?原始性能,还是控制复杂性的能力?和往常一样,这要视情况而定,但考虑到行业向云计算环境的转变,以及与第三方库甚至其他内部部门集成的需求不断增加,功能编程似乎不仅正在流行,而且正在接管。

从这里到哪里去

随着在分布式云环境中并发运行应用程序的需求增加,构建和维护这些解决方案的需求也会增加。我们知道纯 FP 可以扩展,但我们如何使用 FP 来提高总体性能并控制其复杂性?

知识就是力量。继续学习。运用你所知道的去创造一个更美好的未来。

函数式编程资源:

https://www.cambridge.org/core/journals/journal-of-functional-programming/

在这里查看今天的 FP 巨人:

编程语言类别

在这里,我们可以看到四类编程语言。这两大类是命令式和声明式。当用声明性语言编程时,我们告诉计算机我们想要什么。例如,在下面的声明性代码中,我们告诉计算机我们想要找到一辆Highlander汽车。

说明性示例

下面是一个声明式编程语言的示例:

car, err := myCars.Find("Highlander")

与命令式语言相比,所有代码都必须构建一个for循环。

一个必要的例子

以下是命令式编程语言的示例:

func (cars *Cars) Find(model string) (*Car, error) {
  for _, car := range *cars {
     if car.Model == model {
        return &car, nil
     }
  }
  return nil, errors.New("car not found")
}

一个面向对象的例子

面向对象程序OOP)由支持对象相关操作的有状态对象组成,称为方法,其实现和内部结构是隐藏的。这意味着您可以演化或替换对象的内部,而不必更改该对象的客户端。这还意味着,在您不知情的情况下,隐藏数据可能会发生更改,正如我们所看到的,这可能是一件坏事。OOP 还包括继承的思想,在继承中,一个新对象的状态和实现可以基于其层次结构中更高层次的另一个对象,这会导致您的程序变得僵化,更难更改。下面是一个Car对象及其Add方法:

type Car struct {
  Model string
}

func (cars *Cars) Add(car Car) {
  myCars = append(myCars, car)
}

四种编程范例的维恩图

请注意,Go 支持这三种编程风格。最初,惯用的Go 编程风格指导我们使用 for 循环的代码。这种情况正在开始改变。类似地,Java 最初是面向对象和命令式编码风格的混合体。Java 在 2004 年支持泛型,为集合提供类型安全性,并消除类型转换的需要。8 年后,Java 增加了对 lambda 表达式的支持。JDK 的java.util.stream包利用 FP 语言特性,以声明式和并行处理友好的方式提供对数据结构(如集合和数组)的聚合操作。

五代语言

另一种将编程语言分组的方法是通过它们的

第一代(1GL)语言仅由 1 和 0 组成,表示电气开关的开启和关闭位置。人类很难理解 1GL 机器语言。

汇编语言(2GL)允许编程到用户字来表示操作和操作数,例如,CMP 意味着将 AX 寄存器中的数据与数字 99 进行比较。结果存储在 EFLAGS 寄存器中,并由跳转(JL)命令使用。2GL 是特定于特定处理器系列的,也就是说,它们依赖于机器。

3GL 是一种更高级的语言,大多数不依赖于机器。例如,Go 是一个 3GL。Go 比 2GL 抽象了更多的细节,并允许我们使用更熟悉的符号进行编程。Go 提供了花括号{ }来表示代码块、控制结构(如 if、switch 和 range)以及其他抽象(如函数和接口)。

4GL 语言是声明性的。它们允许我们声明要计算的内容,而不是告诉计算机如何计算。这是另一个更高层次的抽象。例如,在 SQL 中,我们可以编写SELECT * FROM USERS,它表示,给我USERS表中的所有列和所有行的数据。我们不需要包括和循环、顺序、解析或任何其他细节,我们只是说了我们想要的。

5GL 语言允许用户使用英语等人类语言编程。它们通常建立在 Lisp 的基础上,模仿人类的特性,如学习、推理、视觉和交流。

第四语言

让我们来看第四种语言。它是必需的,但包含了关键的 FP 方面,如抽象、替换和链接功能。我们可以打开一个 forth 控制台,开始输入命令并获得结果。它内置于语言中,而不是隐藏运行时将使用堆栈在堆栈上推送和弹出运算符和操作数这一事实。没有匿名函数。Forth 使用单词,其行为类似于命名函数。单词可以引用其他单词,这提供了一种非常优雅的抽象形式。Forth 中的常见堆栈操作使用堆栈上最上面的两个或三个值,可以更改事物的顺序或复制事物。

让我们看一个例子:

我们定义一个以冒号开头的函数名/单词。注释用括号括起来。( x -- x-squared )表示我们的函数/字将从堆栈(x)中提取一个输入,并返回该值的平方。我们定义了第二个字,它从堆栈中获取前两个值并返回结果。为了测试,我们键入“3 squared .”,即“.”表示评估此表达式。结果是 9(3 个重复并相乘)。接下来,我们输入2 3 sumOfSquares negate .将 2 和 3 推到堆栈上,执行 squared(由于 3 在顶部,因此返回 9),将 3 与 9 交换,运行 square,取下一个值(2),然后计算“+”,取堆栈上的前两个值(9 和 4)。我们链接内置单词以获得结果:-13。

如果您使用的是 mac,那么您可以使用brew install forth安装 forth。有关详细信息和更多参考资料,请访问:https://github.com/lawrencewoodman/awesome-forth

与 FP 语言不同,Forth 是非类型化的。另外,Forth 直接使用堆栈上的值,而不是传递参数。Forth 是一种占用空间小的编译语言,通常用于嵌入式编程应用程序,例如 NASA 航天器。我们可能不会考虑企业系统开发,因为它缺乏类型安全性。

LINQ 语言

大多数语言都是多范式的,这意味着根据我们的编码风格,我们可以在同一个程序中使用声明性、面向对象和命令式特性。知道什么时候使用哪种风格更像是一门艺术而不是科学。我们学得越多,我们就越有能力做出正确的设计选择,在我们的开发过程中,我们做得越早越好。最后一个问题是,请看从命令式/声明式 FoxPro 到面向对象的 visualfoxpro 的虚线?这是微软扼杀其竞争对手;FoxPro 曾经是一种设计良好的多范式语言。FoxPro 的过程语言使用语言集成查询LINQ进行了扩展。LINQ 在 FoxPro 语言中添加了类似于 SQL 的查询表达式。例如,“散布”和“聚集”命令用于操作数据库表的预烘焙上下文:

select User
scatter memvar
select Customer
gather memvar

这些第四代语言4GL的功能提高了开发人员的生产率和代码一致性。

类型系统

当我们看到*类型这个词时,会想到什么?*数据类型?例如整数、字符串、日期或可以包含多种数据类型的多个字段的复合类型(Go 中的结构)。。

它们有什么用?当我们编译程序时,强类型语言编译器可以捕获可能导致运行时错误或更糟糕的错误结果的错误,这些错误不会使程序崩溃。例如,JavaScript 使用类型强制在运行时动态更改变量的数据类型。对账单我的余额+100.00将等于我的余额 100.00,,这可能不是我们真正想要的,并且可能会导致在线银行客户投诉他们的余额不合算的问题。弱类型语言(如 JavaScript 和 Ruby)需要比强类型语言更严格的测试。类型系统不仅可以在运行程序之前检测程序中的错误,从而提高代码质量,还可以帮助 IDE 提供有用的代码导航功能。

Lambda 微积分

Lambda 演算是一个逻辑规则系统,用于使用变量绑定、抽象和函数应用程序表示计算。我们可以定义匿名函数并应用这些函数。如果不是递归,Lambda 演算将受到限制。源于 lambda 演算的纯函数式编程语言包括 LISP、Haskell 和 ML。

表达式

lambda 表达式是由一组术语组成的函数接口的实例。这些术语可以是变量,如xyz。这些不是变异变量,而是值或其他 lambda 术语的占位符。x 内部的变量应用于它绑定到的任何对象。变量x在术语t内。lambda 抽象定义为λx.t

例如,如果我们有方程f(x) = x2并用 5 替换 x,我们有f(5)=52

当函数f应用于x时,我们得到 x2。在我们的示例中,函数 f 应用于参数 5,得到 52。

为了简洁起见,我们可以去掉括号,将术语f应用于另一个术语 5:f 5 = 52

当我们提取时,我们删除了我们不需要的信息:x 的 Lambda,其中 x2应用于 5(λx.x2) 5 = 52

我们可以用一个不是常数或变量的项来代替 5。x 的λ,其中 x2 应用于 y+1 的λ(λx.x2) (λy.y + 1) = λy.(y + 1)2

现在,我们有了一个新函数。我们将一个函数传递给一个函数,得到了一个函数。

因为 lambda 表达式是函数接口的实例,所以当我们将代码当作数据来编写时,我们实际上是在用代码生成代码。

当我们只需要使用一次函数时,不给函数命名通常更方便。在这种情况下,它将是一个匿名函数。匿名函数的另一个名称是lambda 表达式。当我们可以简单地使用 lambda 表达式时,为什么要创建一个新的局部函数,然后引用命名函数?

匿名函数示例和类型推断

首先,让我们看看术语匿名函数的含义。

Go 中的函数文本要求我们声明其类型(在前面的示例中为int。在 Haskell 甚至 java8 及以上的纯函数式语言中,这些语言的编译器能够推断 lambda 表达式的类型,而无需使用内联声明。这些编译器需要最少的信息来推断运行时的表达式类型。如果编译器看到一个带有参数 5 和“+”运算符的表达式,则具有类型推断的语言将不要求我们明确指出我们正在处理整数。

在这里查看 Java8 中 lambda 表达式类型推断的示例:https://www.youtube.com/watch?v=a8jvxBbswp4

Lambda 表达成分

lambda 表达式是带有参数的未命名代码块。

lambda 表达式由三部分组成:

  • 一段代码x+2
  • 参数x
  • 自由变量值(代码块内未定义)5

Lambda 演算使用以下三个概念来描述如何执行计算单元:

  • 抽象(定义一个函数
  • 绑定(定义一个变量
  • 应用程序(执行功能

未绑定的变量称为自由变量。通过执行简化的单个步骤来实现计算:

  1. α还原
  2. β还原
  3. Eta 降低

考虑下面的非类型 lambda 演算语句:

(λx.xx)(λx.xx)

lambda 符号(名称来源)“λ”绑定名称。在本例中,第一个括号捕获了绑定名称 x 的语句。第二个括号用作论点。在 beta 缩减期间,当我们应用函数时,参数绑定到名称 x。这只是替代。

困惑的这是可以理解的,因为我们混合使用希腊语和英语来描述代码的功能。为了清晰起见,让我们来看一些执行以下步骤的 Go 代码:

请参阅Lambda 演算简化步骤帖子,了解这 3 个步骤的更详细描述:https://stackoverflow.com/questions/34140819/lambda-calculus-reduction-steps

现在我们已经排除了一些形式主义,让我们看看它在实践中的意义。

可视化 lambda 表达式

这是计算 lambda 表达式时发生的情况:

让我们描述一下我们的可视化。

首先,我们将我们的函数定义为a+b抽象操作。此操作需要两个值,a 和 b。其次,当我们执行add2:=add(2)时,我们将值 2 绑定到变量 a。(a从技术上讲是一个变量,但我们将其视为常数。记住?函数编程不允许变异。)由于我们的内部匿名函数在 a 上关闭,a 变量的值存储在我们的闭包结构的上下文中,并且在我们应用b时仍然可用最后评估我们的a+b表达。

我们将 add 函数定义为lambda类型,即接受 int 并返回 int 的函数(注意,与需要两个值的抽象 add 操作不同,我们所有的函数只接受一个参数并返回一个值)闭包结构的输出返回一个表示函数定义 f(b)=2+b 的表达式。

我们在执行three:=add2(1)时调用闭包,其中three是 lambda,即接受输入函数的函数。该输入函数接受一个 int,在我们的示例中是 1。1 与未绑定的终端b绑定。既然我们知道我们所有的变量都是有界的,也就是说,它们都有值,我们可以计算表达式2+1并返回结果3:

以下是输出:

Pass 1 to to add2 expression to get: 3
Pass 2 to to add2 expression to get: 4

步骤 1中,我们定义了add函数。add函数接受int类型的参数a并返回 lambda 类型的匿名函数。

步骤 2中,我们调用 lambda 函数并传递整数 2。2 被接受为参数 a。我们可以说在这一步中部分调用了 add,并且存储在 a 中的值 2 是 curry。我们返回的是一个闭包,即关闭变量的函数。

步骤 3中,我们将自由变量 1 传递给add2lambda 函数。这就是魔法发生的地方。add2是包含当前值为 2 的函数的变量。当我们将 1 传递给该 lambda 时,它将 1 分配给 free 参数,然后将其分配给内部匿名应用程序函数的b参数,在该函数中计算并返回a+b表达式。

很酷吧?Go 允许我们直接实现 lambda 表达式。也许有一天,这个 lambda 闭包应用程序功能将成为 Go 标准库的一部分。这里没有太多的代码,但是理解它然后实现它是一项挑战。然而,现在我们有了它,我们可以重用add2函数并像变量一样传递它。包含上下文数据和逻辑的变量。含糖的

假设我们的例子是初步的,但是考虑一下我们在我们的武器库中所有的自然可伸缩的重用和组合能力。

Lambda 微积分就像巧克力牛奶

封口就像玻璃丸,巧克力糖浆就像我们的咖喱变量*a。*每一个部分装满巧克力糖浆的小酒杯就像我们搁置的部分调用的 lambda 表达式,只是在等待牛奶。

当我们加入优质的奥利牛奶并搅拌时,就像通过 1 并执行 2+1。结果(即,3)是一种称为巧克力牛奶的美味佳肴。对于乳糖不耐受的朋友,我们可以喝一杯巧克力糖浆(部分功能与咖喱巧克力糖浆一起调用)并添加杏仁牛奶。对于我们疯狂的乳糖不耐症叔叔,我们可以再喝一杯咖喱巧克力糖浆,加上大麻牛奶。看,Lambda 演算毕竟并不令人困惑;真好吃!

其他语言中的 Lambda 示例

让我们看看其他几种语言中相同的add2lambda 函数:

JavaScript

由于 JavaScript 是弱类型语言,我们不需要指定我们的ab变量的类型是整数:

var add = function (a) {
  return function (b) {
      return a + b;
  };
};
add2 = add(2);
JavaScript(ES6)

ES6 提供了箭头函数(也称为胖箭头函数),为编写函数表达式提供了更简洁的语法。粗箭头表示匿名函数,允许我们不键入关键字functionreturn

const add = a => b => a + b;
add2 = add(2);

红宝石

让我们学习 Ruby 中的 lambda 表达式;很有见地。

Ruby 允许我们用两种方式定义匿名 lambda 函数。一个使用lambda关键字:

add = lambda {|a, b| a + b}

另一个使用刺伤符号:

add = -> a, b{a + b}

在 IRB 控制台中,我们可以这样调用 lambda 表达式:

>> add.call(2, 1)
=> 3

我们可以用 Ruby lambda 做很多事情。Ruby lambda 是一种特殊的闭包。与 Ruby 块和进程一样,Ruby lambda 的行为类似于可以传递的代码片段。

在现实世界的应用程序中,我们经常在哪里看到 lambdas 与 Ruby 一起使用?曾经和 Rails 一起工作过吗?RubyonRails 是一个 web 应用程序框架,具有名为ActiveRecord对象关系映射ORM库)。ActiveRecord::Base类型的 Ruby 类映射到数据库表。我们称这些 Ruby 类为模型。他们有一个名为scope的方法,用于从关联表中检索行。我们可以使用 lambda 定义范围,如下所示:

class Make < ApplicationRecord
end

class Car < ApplicationRecord
    belongs_to :make
    scope :by_make, -> (id) { where(:make_id => id) }
end

考虑播种我们的桌子,如下:

Make.create({name: 'Lexus'})
 Make.create({name: 'Honda'})
 Car.create({make_id: 1, model: 'IS250'})
 Car.create({make_id: 2, model: 'Accord'})
 Car.create({make_id: 2, model: 'Highlander'})

我们可以使用by_make范围仅检索包含本田汽车的记录,如下所示:

>> ar.by_make(2)
Car Load (1.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."make_id" = $1
+----+---------+------------+
| id | make_id | model      |
+----+---------+------------+
| 2  | 2       | Accord     |
| 3  | 2       | Highlander |
+----+---------+------------+

在前面,我们可以将 scope 方法传递给 lambda 函数的 Honda(2)的 keyid值传递给 lambda 函数。

为了充分利用 Ruby 中 lambda 表达式的强大功能,我们需要修改函数。例如,为了像在 JavaScript 示例中那样使用一个参数调用前面的add函数,我们添加了curry方法来为匿名 lambda 函数创建词法范围。接下来,我们将其存储在名为add2的变量中:

add2 = add.curry.call(2)

lambda 提供了一个闭包,即一个匿名的第一类文本函数,我们将其存储为变量add。curry 添加了一种特殊功能,可以访问创建 lambda 的作用域的其他局部变量。

我们可以通过执行add2变量的 call 方法调用其 lambda 表达式:

>> add2.call(1)
=> 3

查看以下对匿名函数的调用:

>> add.call(2, 1)

这与下面对 curried 函数的调用有什么明显的区别?

>> add2.call(1)

Curried 函数接受一个参数。

为什么使用 curry 而不是具有多个参数的常规函数?

A:您可以向常规函数传递多少个参数?

在这种情况下,严格设置为 2。然而,如果我们使用咖喱,我们可以很容易地添加更多而不破坏我们的界面。这是我们合成工具箱中的一个强大工具。我们可以轻松地用更易于重用的函数替换函数调用链中的各个部分。

因此,我们了解到 lambda 表达式是一个 curried 匿名函数。我们刚刚看到了这两个概念(匿名和 curried 函数)是如何在 Ruby 中定义和访问的。在其他语言中,如 Go,虽然语法不同,但概念保持不变。

类型系统对 FP 的重要性

类型系统的目的是通过定义程序中不同功能之间的接口并验证这些功能是否可以可靠连接来减少 bug。类型可以是简单的字符串、整数和布尔值,也可以是带有嵌入式字段和接口的复杂数据结构。可以在编译时或运行时检查类型。

Lambda 演算最初是非类型化的,但阿隆佐·丘奇发现,尽管它更具表现力,但它导致了不一致。因此,Church 引入了一个类型化版本来简化计算。我们使用类型系统也是出于类似的原因,也就是说,改进确定性并帮助防止 bug。

因为在 FP 中,函数是一种数据类型,所以我们需要为类型系统定义函数的类型。

类型系统还可以提高程序的运行时性能。Go 是一种静态编译语言,因此数据类型在编译时是已知的。这使得类型擦除成为可能。因此,Go 不需要我们的程序携带显式类型注释。将此与支持泛型的语言进行对比。泛型使用一个称为具体化的过程,该过程允许程序员传递泛型数据类型以及显式类型注释,以便需要知道其类型的被调用函数可以使泛型数据成为一级公民,也就是说,将其转换为程序识别的实际数据类型。

具体化增加的复杂性和使用泛型的性能下降与 Go 的简单性和性能的核心原则相矛盾。

静态类型与动态类型

在 GO 和其他静态类型语言中,如 C、C++、java 和 Scala,编译器将在编译时捕获类型不匹配。相反,Ruby、SmallTalk 和 Python 等动态类型语言在运行时捕获这些类型错误,并更多地依赖错误处理来防止程序崩溃。

在静态但动态类型化的语言中,我们可以轻松编写函数定义,而不必提及数据类型,如下所示:

def add(a, b)
    a+b
end

当我们传递正确的数据时,这非常有效:

>> add(1,2)
=> 3

但是,当我们传递兼容的类型时,会发生运行时异常:

>> add(1,Time.now)
TypeError: Time can't be coerced into Integer

类型推断

类型推断是根据表达式的使用方式确定其适当类型的过程。

Go 可以确定以下示例中变量 a 的类型为int

var a = 5
a := 5

Go 在许多情况下都能正确推断数据类型,例如:

a := 1.8
b := math.Floor(a + 1)
fmt.Println("b:", reflect.TypeOf(b))

以下是输出:

b: float64

但是,由于 Go 没有完全实现 Hindley-Milner 类型系统,Go 无法推断本例中b的类型:

a := 1
b := math.Floor(a + 1.8)
println(b)

Go 报告以下编译错误,而不是推断b的类型是 float64:

constant 1.8 truncated to integer
cannot use a + 1.8 (type int) as type float64 in argument to math.Floor

不幸的是,Go 的类型系统实现并不完美,但可以理解为什么它没有完全实现 HM 类型系统。HM 支持多态函数。Go 既不支持泛型或多态函数,也不支持参数多态性。然而,对于任何未知类型,使用interface{}可以在 Go 中实现多态列表操作。我们可以将其存储在interface{}的切片中,即[]interface{}中,并在列表上使用正常的切片操作(追加、复制、移位等)。当我们稍后从切片中检索它们时,我们需要将这些项转换为相应的类型。

哈斯克尔

函数式编程的普及很大程度上得益于 Haskell(以 Haskell Curry 的名字命名),这是一种编程语言,由一群非常熟悉范畴论的学者设计。由于 Haskell 语法非常清晰,并且与原始的形式逻辑符号非常一致,我们可以在下面的文本中看到一些示例来帮助表达范畴论概念。

哈斯凯尔的情况和 Go 有点不同。例如,Haskell 变量是不可变的,也就是说,它们不允许更改。我们仅将它们用作表达式的绑定。

我强烈建议学习哈斯克尔。这是一种很棒的、纯粹的函数式编程语言。这里有一些很好的资源可以帮助您开始:

在 Haskell 中,我们没有实现算法中的步骤。相反,我们声明函数的作用。考虑下面的例子:

  • 一系列数字的总和是零加上所有数字的总和
  • 一组数字的乘积是所有数字乘积的 1 倍
  • 一个数的阶乘是从 1 到该数的所有数的乘积
  • 我们的新列表是将原始列表中的所有数字加上两个的结果

在 Haskell 中,我们的函数只能计算一个值并返回它。此功能支持引用完整性。如果使用相同的参数多次调用函数,则保证每次都返回相同的结果。这允许编译器对程序的行为进行推理并提高其性能。此功能还允许我们将函数组合在一起,以构建更复杂的函数。

Haskell 将那些不必要的语法和代码简化成了一句话。

学习一点 Haskell 将有助于我们了解新的函数式编程范式,我们将在第 10 章单群、类型类和泛型中介绍这些范式。

Haskell 中的类型类

Haskell 是强类型的,完全支持 HM 类型系统。Haskell 在我们通常认为的类型之上还有一层。回想一下,类型定义了存储在该类型的变量中的数据结构(stringint、用户定义的结构,等等)。类型类允许我们更加具体,不仅可以指定数据是什么,还可以指定数据的行为方式。

类型类定义操作集。一个特定的对象可能是一个类的实例,并且将有一个对应于每个操作的方法。类型类可以分层排列,形成超类和子类的概念,并允许继承操作/方法。默认方法也可能与操作相关联。

类型类不是对象;没有内部可变状态。类型类是类型安全的;对类型不在所需类中的值应用方法的任何尝试都将在编译时检测到。换句话说,方法在运行时不会被查找,而只是作为高阶函数传递。

与接口声明一样,类型类声明定义使用对象的协议,而不是定义对象本身。例如,如果某个类型由另一个类型参数化,并且可以使用 fmap 函数修改其值,则该类型就是 Functor 类的实例。

在这里查看 Haskell 的类型类层次结构,我们可以看到 Monad 是一个单群,也是一个应用程序。因此,我们知道 Monad 继承了这两个函数的操作。

因此,我们不需要将 int 类型添加到参数签名中,我们仍然可以获得类型安全特性来在编译时捕获错误。以下定义了添加 2 的 lambda 函数:

(\a -> a + 2)

在下面的示例中,我们在 Haskell REPL 控制台中,可以在其中以交互方式输入 Haskell 命令:

lambda 字符允许我们定义一个执行 curry 操作的匿名函数。我们将 lambda 函数传递给 map,这是一个高阶函数。Map 将原始列表中的每个元素转换为一个新列表,该列表通过向列表中的每个项目添加 2 而产生。

域、余域和态射

如果我们仔细观察,我们可以在我们周围找到有序的数据对。让我们看看莱昂内尔·梅西的一些统计数据。下表显示梅西连续 10 年攻入多少球:

我们说域是集合 A:{2007, 2007, 2007, 2010, 2011, 2012, 2013, 2014, 2015, 2016},范围(或密码域)是集合 B:{5, 6, 7, 8, 10},有序对是{(2007,10), (2008, 6), (2008, 8), (2010, 5), (2011, 8), (2012, 5), (2013, 5), (2014, 7), (2015, 6), (2016, 10)}

每年都有许多进球。

如果通过调用名为f的函数计算 x 和 y 的年份,我们可以通过调用 f(x)得到 y。例如,f(2010)=5f(2016)=10

以下关系有意义吗?

梅西怎么能在同一年里进 6 球,7 球,10 球?那没有道理,对吧?(对!)

我们可以说,由我们的箭头定义的*{(2007,6)、(2007,7)、(2007,10)}的关系不是一个函数,因为它包含具有相同x*值的有序对。

集合论符号

在继续范畴论之前,让我们先熟悉集合论的符号:

| 符号 | 符号名称 | 含义/定义 | 示例 | | { } | 设置 | 对象的集合(也称为元素) | A={5,6,7,8},B={5,8,10} | | | | 以致 | 因此 | *A={x | x∈*ℝ, x<0} | | A.∩B | 十字路口 | 属于集合 A 和集合 B 的对象 | A∩ B={5,8} | | A.∪B | 协会 | 属于集合 A 或集合 B 的对象 | A∪ B={5,6,7,8,10} | | A.⊆B | 子集 | A 是 B 的子集。集合 A 包含在集合 B 中 | {5,8,10}⊆ {5,8,10} | | A.⊂B | 真子集/严格子集 | A 是 B 的子集,但 A 不等于 B | *{5,8}⊂ {*5,8,10} | | A.⊄B | 非子集 | 集合 A 不是集合 B 的子集 | {8,15}⊄ {8,10,25} | | A.∈A. | 元素 | 设置成员 | A={5,10,15},5∈ A | | x∉A. | 非元素 | 无固定成员 | A={5,10,15},2∉ A | | (a、b) | 有序对 | 两个元素的集合 | | | A×B | 笛卡尔积 | A 和 B 中所有有序对的集合 | | | |A| | 基数 | 集合 A 的元素数 | A={5,10,15},|A |=3 | | Ø | 空集 | Ø = {} | A=Ø | | ↦ | 映射到 | f:a↦ b 表示函数 f 从元素 a 映射到元素 b | f:a↦ f(a) | | U | 通用集 | 所有可能值的集合 | | | ℕ o | 自然数/整数集(带零) | ℕ o={0,1,2,3,…} | 0∈ ℕ o | | ℕ 1 | 自然数/整数集(不含零) | ℕ 1={1,2,3,4,…} | 5∈ ℕ 1 | | ℤ | 整数集 | ℤ = {... -2, -1, 0, 1, 2, ..} | -5∈ ℤ | | ℝ | 实数集 | ℝ= {x|-∞ < x | 5.166667∈ ℝ |

在集合论中,我们研究集合中的元素。例如,集合A可以有 2 个元素:{5, 6},集合B可以有 3 个元素:{7, 8, 10}。笛卡尔乘积具有每种可能的组合:{(5, 7), (5, 8), (5, 10), (6, 7), (6, 8), (6, 10)}

在范畴论中,我们不再关注集合内部的元素,我们只关注集合之间的关系。换句话说,我们只看箭头。

范畴论

范畴论是数学的一个分支,它研究的是结构,而不是细节。它处理使程序可组合的各种结构。

范畴论是数学的一个分支,类似于集合论。类别的一个基本示例是集合类别,其中对象是集合,箭头是从一个集合到另一个集合的函数。类别需要的对象通常是集合,箭头通常是函数。任何形式化数学概念的方法,使其满足对象和箭头行为的基本条件,都是有效的类别。

我找不到一个容易理解的学习范畴论的资源。大部分的东西都是面向数学家的。虽然我在大学里上了很多高等数学课,但我不是一个实践者。虽然理解逻辑和数学形式主义很重要(我们将涵盖足够熟悉的内容),但我真正想要的是一些我可以绞尽脑汁的东西。我想要实用的信息。我想知道,如何在 Go 中实现这个 Lambda 演算?如何使用这些 lambda 构建更好的可伸缩软件?我怎样才能把细节区分开来,从更小、更简单的部分组成一个更好的应用程序?我能否利用这些新发现的知识更好地构建我的大数据/数据分析项目?我希望这一章能为你做到这一点。

函数代数

范畴论是抽象的函数代数。事实上,Lambda 演算是一种用于指定、操作和计算函数的演算。Lambda 演算和范畴论之间有着深刻的联系。我们从两个不同的角度来看同一件事——从 Lambda 微积分的逻辑、句法角度,以及从范畴论的代数、几何角度。

抽象函数

抽象函数是任何可以以函数方式读取的过程、表达式或赋值。这是一个抽象函数的抽象代数。

我们将研究集合上的集合理论函数,以得出范畴论的基本原理。

我们来看看片场的函数。给定集合为ABC。以及从aB的功能f

f:A->B

函数的正式定义

A 函数是 A 和 B 的笛卡尔积的子集,是AxBA 交叉 B的关系:

f 等于或是 AxB的子集

这里,f 是对的子集。

对于所有 A,存在唯一的 B(B:B),使得子集*<A,B>*是该关系 f 的一个关系:

<a、b>∈ f

函数的直观定义

以更直观的方式,我们将函数 f 视为:获取集合 a 的元素并返回集合 B 的元素

带集合的函数合成

函数合成是我们获取一个函数的输出(f:A)→ B并将其用作另一个(g:B)的输入→ C)。通过结合性定律,我们知道如果A→ B→ C,那么这是真的:A→ C。(我们可以从 A 到 B 再到 C,也可以从 A 直接到 C。)

使用差旅费的合成操作示例

在下表中,我们输入了从美国到欧洲的旅行预算:

如果我们从美国旅行到欧洲,我们使用f箭头(函数)将美元转换成欧元。如果我们从欧洲旅行到墨西哥,我们使用 g 箭头将欧元转换成比索。这里,函数 f 的输出是函数g的输入;这称为函数合成。

如果我们决定不往返欧洲,不直接从美国前往墨西哥,我们将使用 gof 箭头。无论是f($) → g(€) → ₱还是f(g($)) → ₱,我们的美元都应该得到同样数量的比索!

类别

类别由其对象和连接对象和所有组合的箭头定义。

对于每两个箭头(f 和 g),我们必须定义它们的组成(gof

范畴论的要素/数据包括:

  • 类别/集合:是一组对象
  • 对象:点/点/无属性无结构的原语
  • 态射:(箭头)介于两个对象/元素之间的事物

我们用大写字母(如 A、B、C 等)书写对象。我们用小写字母写箭头(如 f、g、h 等)。

箭头有起点和终点。箭头开头的对象位于域中;末端箭头处的箭头在范围内(也称为编码域)。

范畴公理

对于每个 f,我们都有一个箭头,从 f 的域指向f的辅域:

f:dom(f)→ 化学需氧量(f)

对于每个 A,我们都有一个从 A 到 A 的标识箭头:

1A:A→ A

对于每个可组合对,A→ B→ C我们有一个来自a 的合成操作→ C

分类法

以下是分类法:

  • 结合性HO(GOF)=(HOG)OF
  • 身份FO1A=f=FO1B
  • 单位:每一个复合物都等于它自己

我们将在本章后面更仔细地研究这些法律。

更多规则

以下是适用于类别的更多规则:

  • 在对象之间可以有零个或多个箭头。
  • 域中的任何对象最多只能有一个箭头。回想起 x 值不得重复。
  • 我们可以将所有合成放在合成表中(如何合成态射)。
  • 不同的作文会给你不同的分类。
  • 对象和箭头没有结构和信息;这篇作文包含了信息。
  • 范畴论是以更一般的概念为基础的。
  • 对象和态射的 s 值。对象泛化类型,态射泛化函数。
  • 类别不考虑时间。
  • 物体之间也存在空间关系。

说到编程和计算机,时间很重要。例如,如果我们正在研究足球在飞行中的运动,那么足球相对于时间在三维(x,y,z)空间中移动。如果我们想知道球相对于时间的确切位置,我们需要在计算中考虑时间。

更多例子

这里有几个例子可以帮助你更好地了解类别是什么,类别意味着什么,需要什么,以及必须遵守什么规则。

无效类别

这里,我们有两个有效的类别。第一个是一辆汽车。这些对象包括汽车本身、汽车型号名称和汽车年龄。我们展示了两个恒等态射。一支箭从一辆车射向它自己(升级一辆车,它就是另一辆车)。另一个箭头从 integer 对象指向其自身(“++”运算符表示将一个添加到当前值)。我们省略了从模型名称到自身的箭头,但它存在(名称就是名称):=

为什么这是无效的?看起来像是在作曲,但真的吗?

下一个例子应该更明显一点。(有趣,但显然不是一个类别。)

有一个从收藏夹页面(a)到 Reddit 主页(B)的链接,还有一个从那里到图像(C)的链接,但没有一个从收藏夹页面(a)到图像(C)的链接。

态射

态射是一个类别(A,B,C 的分组)中一个对象(在我们的示例中是 A,B,C)的箭头。从 A 到 B(或从 B 到 C,或从 A 到 C)可以有多个箭头。此外,箭头可以从任何物体指向自身;这称为同一态射。

  • f:A→B 语句是从 a 到 B 的态射(f)
  • Hom(A,B)是从 A 到 B 的所有箭头的集合
  • Hom(A,B)也称为 A 到 B 的 Hom 集合
  • 艾达:A→A 是从 A 到 A 的态射

态射的行为

让我们看看我们可以用态射做的几件事。W 可以组合它们并运行身份变形来验证对象的身份。

合成操作

下面是我们的基本合成操作。

合成操作为gof、**g,在 f应用 arg x(从A开始)后,将 g 应用于 f,并应用于x(gof)(x)=g(f(x))。

如果所有xf(g(x))=g(f(x)),那么我们可以说fg在组合下通勤。

然而,这并不典型。函数组合通常是不可交换的。

让我们举个例子。还记得我们在本章前面用g(x)=x2+1组合f(x)=x+2的时候吗?我们解出了g(f(1))=10,但是*f(g(1))*呢?这也等于 10 吗?现在让我们看看:

g(1)=12+1=2f(2)=4

所以,不,我们的函数 f 和 g 是不相关的:g(f(1))!=f(g(1))

身份操作

我们范畴的恒等式定律说 A 的恒等式态射是 A。

每个对象都有一个指向自身的态射。

当我们有多个对象时,我们用一个下标来表示我们正在谈论的 ID,例如,idA

这张图上写着fo idA=f

换句话说,idA之后的f的态射与f的态射相同。下面是一个具体的例子:

自然数 3 的恒等态射是一个将任意数乘以

存在一个对称的同一态射:IDAOG=g

结合定律

在下图中,我们可以通过组合的GO 从 A 到 C。

C开始,我们可以使用h箭头到达D,我们可以将其写成ho(gof)

注意,这与**h(f(g))**相同。这种表示法似乎比使用合成操作更直观,但它们的意思是相同的。

从下图可以看出,ho(gof)与的(hog)相同。

因此,我们的范畴遵循结合性定律。下一个图表是组合关联性的另一个说明:

该图显示,如果箭头存在于→B→C→D、 然后,如果我们从 A 开始,我们可以使用函数组合,通过选择的红色路径ho(gof)或绿色路径(hog)来缩短集合。

关联性帮助我们管理复杂性。这是作文的基础。

只与态射有关

在范畴论中,我们只有对象和箭头。

我们可以通过将函数应用于参数来组合函数以获得结果。然后,我们对结果应用另一个函数,依此类推,直到我们到达开始的地方。

我们把所有的构图都放在一张表中,只关心态射。正是这些态射为我们的应用程序定义了接口。重要的是对象的连接/映射方式。

接口驱动开发

我们在开发软件时可以使用的范畴论的一个概念是,我们的设计应该只关注接口,即箭头/变形。我们在这本书中看到了作文的主题反复出现。从莫扎特作曲到线性和二次函数的函数作曲,再到有限状态机。我们已经看到,解决复杂问题的方法是将它们分解成可以理解的部分。然后,我们可以进入我们的工具箱,编写优雅、可靠的解决方案。我们设计了我们的应用程序编程接口API)来连接我们的各个部分,并且可以利用并发编程结构和并发感知框架来安排各个部分如何协同工作,以达到我们期望的结果。

设计架构,命名组件,记录细节。清晰胜于聪明。不要通过分享记忆来交流;通过交流共享内存。渠道协调;互斥体序列化。接口越大,抽象越弱。

更多 Go 谚语,请访问:https://www.youtube.com/watch?v=PAAkCSZUG1c ,*并发不是并行,*访问:https://www.youtube.com/watch?v=cN_DpYBzKso

更多态射

下面的例子显示了从 a 到 B 的两个恒等态射和一个态射。

如果我们将 A 和任何 f 取为 B,并且A(1A)上的恒等式,那么在 A(fo1A上的恒等式之后的组合 f 等于 f。

下面是另一种看待它的方式:

如果我们取A和任何f并取A上的恒等式,那么A上恒等式之后的这个复合f等于f

这是从 A 到 B 的态射 f:

下面是一个具体的例子:

恒等式公理说,如果从AB的恒等式有一个箭头 f,从BB的恒等式有一个箭头 f,那么从AB的箭头是相同的箭头f

结合性公理说箭头的组合是结合的,这是表示图表是可交换的另一种方式。

因此,对于所有箭头,𝑓 ∶ 𝑎 → 𝑏, 𝑔 ∶ 𝑏 → 𝑐, 和 h∶ 𝑐 → 𝑑, h∘ 𝑔 ∘ 𝑓 表示h∘ (𝑔 ∘ 𝑓).

这是真的:h∘(𝑔∘𝑓) = H∘𝑔∘𝑓 = (h)∘𝑔)∘𝑓.

范畴论述评

范畴论是关于构成函数的。

A,B,C=类型=代数/数学结构(同态)

请注意,我们不再关心集合中的对象/元素(仅箭头)。

f=函数=对象之间的箭头(并保持代数结构)

f变量是一个接受 a 类型参数的函数,例如,可以返回 B 类型的对象。

身份箭头艾达)从 A 到 A,什么也不做。f;g由一个箭头接一个箭头组成)是一个函数,它接受 a 和 B 类型的参数,并返回 C。

艾达;f=f;idB=f

有三种方式组成两件事:(f;g);h=f;(g;h)

C(C 类)=设置 A 到 C 类中所有箭头的在 C 中。

更多的通信

还记得我们在第 6 章构建在洋葱架构(i通过流水线提高性能中讨论的基于流的编程中的过滤器类型(读取、拆分、转换、合并和写入)吗?让我们看看基于流的编程如何与范畴论、逻辑和类型相对应。

  • 逻辑具有 and、or 和蕴涵运算
  • 编程有数据记录和函数操作
  • 基于流的编程具有合并、拆分和转换操作

范畴论的箭头是函数(也可以是数据)。

就像逻辑中的命题可以是真的也可以是假的一样,类型也可以是有人居住的,也可以是无人居住的。真正的命题是有人居住的。void 类型为 false。如果我们能产生一个类型的元素,那么我们已经证明了我们的命题。

态射表

下表总结了我们的基本操作以及初始和终端状态:

态射示例

a→ b语句表示,如果我们为函数提供一个元素 a,那么我们的函数将产生一个元素 b。逻辑含义也是如此:如果 a 为真,那么 b 为真。

如果我们有一个函数类型a⇒ b并将其与a元素配对,我们得到一个 b 元素。

调制解调器

在拉丁语中,modens ponens 的意思是“肯定的方式”。

类型理论版本

((a)⇒ b) ,a)→ b表示如果我们有函数(a⇒ b) 一个参数 a,它将产生 b。

逻辑版本

如果我们知道 b 来自 a,并且 a 是真的,那么你可以证明 b。

a⇒ B∧ A.→ b

这被称为 Modens ponens,也称为暗示。

逻辑与类型理论的对应

你看到逻辑和类型理论之间的一一对应了吗?

加上范畴论对应关系,我们得到了 Curry-Howard-Lambek 对应关系。

笛卡尔闭范畴

笛卡尔闭范畴是逻辑和类型理论的模型,其中任意两个元素存在乘积,任意两个元素存在指数。

虽然许多类别都有乘积和和和,但只有少数类别有贴图对象。这类范畴称为笛卡尔闭范畴。

λ演算、逻辑学和笛卡尔闭范畴之间有着深刻的联系。

笛卡尔封闭范畴CCC)是一个抽象概念,具有少量词汇表和相关定律:

范畴部分意味着我们有态射的概念,每个态射都有一个域和余域对象。和结合合成算子有一个恒等态射。

笛卡尔部分意味着我们有乘积,有投影函数和一个操作符(fstsnd在 Haskell 中)将两个函数组合成一个成对产生函数

闭合部分意味着我们有一种通过对象表示态射的方法,称为指数

相应的操作是 curry 和 apply。这些指数对象是一类函数。

Lambda 表达式可以系统地翻译成 CCC 词汇表。

CCC是一个关于乘积和指数都是封闭的类别。

这就是它在产品和对象总和方面的外观:

a × (b + c) = a × b + a × c
(b + c) × a = b × a + c × a

参见与下列分配定律的对应关系?

(a∨ (b)∧ c=(a)∧(c)∨ (b)∧ c)

CCC 中的对象表示语言的类型,例如字符串、整数和浮点。态射表示可计算的函数,例如长度(字符串)。指数对象允许我们考虑可计算函数作为输入到其他函数。

Joachim Lambek 发现简单型λ-演算(STLC)的模型正是笛卡尔闭范畴(CCC)。

Java 中的泛型类型机制基于起源于 Lambda 演算的泛型类型系统。事实上,Java 使用 Hindley-Milner-Lambda 演算类型推断,它基于 CCC。

在后面的章节中,我们将再次讨论 CCCs 的主题。

单位类型

元组是有序且不可变的项的列表。可以根据图元的位置选择图元。

单位类型正好有一个值。它也被称为身份。乘法的单位是 1,加法的单位是 0,字符串串联的单位是空字符串。

定义为类型为int的元组的类型可以包含多少个值?无限的(-∞, …, 0, 1, 2... ∞).

定义为空元组的类型可以包含多少个值?一在 Haskell 中,单元也表示为()

单位类型的值是,您可以在我们可能返回 nil(或 null)的地方使用它。当我们不关心值是多少时,我们返回一个单位。我们不返回 nil,我们返回一个值;单位值。所有函数返回值;不再有空指针异常!现在,我们可以链接函数,而不用担心中间的函数抛出空指针异常并导致程序崩溃。

同态

这是一个维恩图,描述了不同类别的同态如何相互关联:

| 简称 | 说明 | | 单声道 | 单态集(内射) | | 计划免疫 | 满态集(满射) | | Iso | 同构集(双射) | | 汽车 | 自同构集(双射和自同构) |

同态是集合 A(域)和集合 B(辅域或范围)之间的对应关系,因此 A 中的每个对象确定 B 中唯一的对象,B 中的每个对象都有一个箭头/函数/态射从 A 指向它。

如果为 A 和 B 定义了运算,例如加法和乘法,则要求它们对应。也就是说,ab必须对应于f(a)f(b)

同态保持对应

通信必须如下所示:

  • 单值:态射必须至少是部分函数
  • 满射:a 中的每个 a 在 B 中至少有一个 f(a)

同态是比较两组结构相似性的一种方法。这是两个群体之间的一种功能,可以保持他们的结构。假设我们有两个群,G 和 H。G 和 H 有不同的群运算。我们还假设G具有群操作☆ H 组进行心脏手术。给定G 中的任意两个元素:a,b∈ G。让我们假设一个☆ b=c。我们还有一个函数 f,它将G映射到H:f:G→ H。元素 a、b 和 c 映射到 H 中的元素。a 变量映射到 f(a),b 映射到 f(b),c 映射到 f(c):

  • f:a↦ f(a)
  • f:b↦ f(b)
  • f:c↦ f(c)

同态的目的是发现两个群之间的结构相似性。

所以,如果在 G 中,a☆ b=c,那么我们喜欢 H 组中的 f(a)(心脏)f(b)=f(c)。

A.☆ b=c⇒ f(a)(心脏)f(b)=f(c)和☆ b=c,我们可以替换得到同态的定义:

f(a)(心脏)f(b)=f(a☆ (b)

有一种方法可以比较两组。

让我们看一个例子。G 是一组实数(ℝ) 具有加法(+)的组运算符和标识运算符 0,H 是一组实数(ℝ) 使用乘法(*)的群运算符和标识运算符 1。

我们可以定义将 G 的元素映射到 H 的同态,该同态将元素a映射到ea

f:G↦ H

A.↦ ea

让我们通过验证 f(a+b)=f(a)*f(b)来确保这是一个同态。

根据上述 f 的定义,这表示:

ea+b=eaea*

这是真的。这是指数法则。所以,f 是同态。

同态加密

同态加密允许在不知道相应明文的情况下对密文执行操作:

EncryptFcn(a) (heart) EncryptFcn(b) = EncryptFcn(a ☆ b)

同态加密的一个例子

Alice 从不可信的来源下载了一段她喜欢的音乐片段,并想用它来查找歌曲的名称。

Bob 具有歌曲识别功能,可以为 Alice 识别歌曲。

问题是爱丽丝和鲍勃彼此不信任。

爱丽丝担心如果她把自己的一段音乐给鲍勃,鲍勃可能会把她变成当权者。鲍勃可以把他的音乐目录给爱丽丝,但担心她会把它卖给他的竞争对手。

解决方案是让 Alice 加密她的音乐片段并将其发送给 Bob。Bob 可以找到加密结果并将其发送回 Alice 进行解密。

吸取的教训

通过使用密码学和范畴论,我们可以在不泄露私人信息的情况下执行复杂的协作操作。

同构

有时,群体不仅仅是相似的。如果它们是相同的,那么它们是同构的。同构是由两个希腊单词组成的,意思是相等和形式。在数学中,同构是两个群(结构或集合)之间完美的一对一双射映射。A 组中的每个对象都直接映射到 B 组中的一个对象。

在同构中,A 中的每个对象都映射到 B 中的一个对象。同构也是内射的,因为没有两个对象从 A 映射到 B 中的同一个对象。因此,如果 A 中的对象是 x、y 和 z,则以下情况是不可能的:f(x) = f(y), f(x) = f(z), f(y) = f(z。我们找到的唯一映射是x -> f(x), y -> f(y)z -> f(z),这三个值都不相同。

这个态射也是满射的,因为 codomain B 中的每个对象都至少有一个来自域 A 的映射。此外,因为 A 和 B 中的每个对象之间有一对一的对应关系。我们可以看到,我们有满射和注入;这也称为双射。

内射态射

内射态射是指 A 中的每个对象映射到 B 中的不同对象。不允许映射到 B 中的相同对象。

满射态射

满射态射是指余域 B 中的每个对象都连接到域 a 中的态射。换句话说,B 中的每个对象都有 f(x)的值,其中 x 在 a 中。这种映射称为多对一,,因为从 a 到 B 中的单个对象有多个映射。

自同态

如果对象在同一集中,则该态射称为自同态。换句话说,态射映射回自身。一个例子是自然数(正整数)的域 a,由加法和乘法运算组成的态射,以及由自然数组成的 B 范围。另一个例子是 12 小时模拟时钟上的一组数字 1 到 12。

半群同态

半群是具有关联运算的集合。将任意两个正整数相加会产生另一个正整数;因此,加法是自然数的一种结合性质。

另一个例子是具有单位态射的单群,它充当单位算子。例如,自然数和乘法态射的集合,其单位态射是 mutliplyByOne 函数,或自然数和加法态射的集合,其单位态射是 addZero 函数。

半群同态代数

考虑到我们给出了半群(A,*)和(B,+)和函数 f:a=>B,如果 F 是 0,则 F 是半群同态。

请注意,“+”是范围 B 中的操作,“*”是域 A 中的操作。

因此,半群同态是两个半群之间保持半群运算的映射。

同态表

下表包含对应于同态维恩图的同态类别。

A.→ A.

| 态射 | 说明 | | 示例 | | 计划免疫- | 满射“到” | A.→ B⇉ C | | | 单声道- | 内射“1 对 1” | C⇉ A.→ B | | | Iso- | 双射词“on”和“1 对 1” 双射=内射+满射 域中的每个元素都将在该范围内有一个对应的元素(又名“codomain”)。 | A.⇄ B | G 和 H 之间的同构。有序对:f(a)=1,f(b)=6,。。。f(j)=7 | | Endo- | 从结构到自身。 自同态是其域等于范围的同态。 | A.→ A. | 相同的像素,重新排列。 | | 自动- | 双射自同构是一种自同构,也是一种同构;与自身同构。 自同构=双射+自同构 | |

车祸类比

我们从不同的角度看待相同的结构/思想(分解、组合、转换等)。无论是从数学/代数/几何、逻辑/句法、Lambda 演算还是基于流的角度来看,都是一样的。我们只是用不同的符号来表达相同的概念。这有点像问四个不同的人他们在车祸后看到了什么。他们都看到了相同的东西,但表达方式不同。考虑所有的观点可以带来更清晰和更好的理解。

可组合并发

函数式编程不仅仅是关于组合函数和代数数据结构——它使并发可组合——这在其他编程范式中几乎是不可能的。

我们如何将所学的态射知识应用于创建高度并发的处理模型?假设我们从一个具有单个二进制可执行文件的单片应用程序开始。

如果我们只关注系统中的态射,即输入和输出的接口,会怎么样?

考虑我们得到如下:

  • 输入和输出可以通过同构映射
  • 状态存在于对象的分组中
  • 态射是无状态的

有限状态机

我们可以假设我们系统的有限状态机(FSM)存在于我们的分组中吗?(FSM 就像我们之前看到的 A 组和 B 组。)

让我们想象一下,系统地将 FSM 分解为尽可能小的组件。

从我们的上下文组件 C 开始,通过观察行为/态射并应用 Schreier 精化定理和我们对同构的了解,我们可以系统地将大型 FSM 分解为一组具有等效行为的最小可能 FSM 吗?

以下是第一个分解:

下面是第二个分解:

我们一直在并行连接我们的组件;我们还可以(反)按顺序组合它们:

现在,我们有了一套完整的 FSM 组成了我们的系统,我们已经用系统的构建块填充了我们的工具箱。现在,我们可以通过重新连接简单的组件并将它们装配在分布式、基于微服务的环境中来重建系统。一旦我们测试了我们的新配置,我们就可以部署构建块并水平扩展它们。

这种能力和灵活性需要付出一定的代价。为了更好地使用我们的组件,我们必须构建一个框架将它们粘合在一起,并对工作进行排序。我们需要考虑以下几点:

  • 接口的兼容性
  • 决定如何划分我们的工作
  • 调度态射
  • 管理资源

图形数据库示例

假设我们刚被雇佣为当地一所大学构建一个图形数据库解决方案。首先,我们应该建立信息模型。它可能看起来像这样:

每门课程都有一名教师和许多学生。

讲师可以教授多个课程。

一个导师有多个学生,但一个学生只能有一个导师。

一个学生可能有不止一个导师,一个导师可能帮助不止一个学生。

我们有五组对象:

| 设置 | 描述 | | A | 教官 | | B | 课程 | | C | 学生 | | D | 导师 | | E | 顾问 |

我们实际的数据库模式可能如下所示:

运用数学和范畴论获得理解

让我们来做一些与足球相关的事情!

当球从梅西的脚进入球门时,我们怎么知道球在空中的位置?

请注意,由于球的旋转和气压的不平衡,当球在空气中时,球可能会从左向右、向上和急剧向下弯曲。

在这里查看梅西的弧线进球:

假设我们有一个 50 码×100 码的小足球场和一个 8 英尺高的网。目标的高度为 z 维度:

如果太阳直接在头顶上,在磁场上形成阴影,那么我们可以知道 x,y 坐标。如果我们也可以测量球移动时的高度,那么我们知道 z 坐标。结合这两条信息,我们可以知道球在三维空间中的位置。

下图显示,如果我们知道 A 和 B,那么我们也知道 C。

在范畴论中,这被称为两组箭头 f 和 g 的乘积。

假设第一个从 AxB 中提取 A,第二个从 AxB 中提取 B,h=,我们可以说<f,g>; first = f<f,g>; second = g.

我们有一个通勤图。换句话说,我们有两条路径从 C 到 AxB,第一条到 A 等于 f,f 从 C 到 A,反之亦然。

通用性条件表示从 C 到 AxB 的唯一方法是将应用到 C。

该图显示,任何箭头都与 f 和 g 箭头一一对应。

集合(C,A)x(C,B)的笛卡尔积等于集合(C,AxB)。

在逻辑上,这意味着如果 A 是真的,B 是真的,那么我们知道 A 和 B 是真的。所以,乘积是逻辑连词。

表中描述了标识功能。它说 AxB 的身份函数是 AxB。虽然这看起来很简单,但很重要。标识函数也称为单元,我们已经讨论过它与类型的关系,并将在关于单子的一章中再次遇到它。

我们在小学看到了乘法的下列定律:

| 法律 | 示例 | | 身份 | A x 1=A 和 1 x A=A | | 联想 | A x(B x C)=(AxB)x C | | 可交换 | A x B=B x A | | 分配的 | A x(B+C)=(A x B)+(A x C) |

下图描述了类别理论中产品的同一性法则。说明 AxB 的身份是的箭头对:

下图描述了求和操作:

它说有两种方法来建立一个总和,inLeft 或 inRight。

在代码中,我们需要一个 case 语句或 if/then 语句来获得结果。

[f,g]中的括号表示结果为 f 或 g。

如果要得到真和或真乘积,态射必须终止。

我们的图表还表明,从 A+B 的箭头与从 A 到 C 的箭头同构,与从 B 到 C 的箭头配对;或者形式上,(A,C)x(B,C)=(A+B,C)

所以,如果我们知道 A 或 B,我们就有 C 的证明(这也被称为析取)

集合论方程为A ∪ (B∩C) = (A∪B) ∩ (A∪C)

有关分配法的更多信息,请访问:https://ncatlab.org/nlab/show/distributive+法律

建立 lambda 表达式的指数定律

构建 lambda 表达式的指数定律如下所示:

表格图例

Gamma(Γ)表示环境,即自由变量与一组类型配对。我们给自由变量命名(例如,上下文中类型 A 的 x,类型 B 的 y,…)及其类型。

旋转栅门(⊢) A.⊢ b 表示 a 可以从 b 证明。

术语(M)表示我们用编程语言编写的表达式。

我们展示了 A 型和 B 型。

对于右上角的法律。。。

考虑 F 代表语义(上文)和咖喱(F),我们将 F 和 G 配对,得到 A 到 A,并应用它,明确地,指数给我们暗示 A。⇒ B

对于左下角定律:

我们怎么知道 A 意味着 B?通过假设 A,我们可以证明 B,我们知道 A 意味着 B。

如果我们知道 A 意味着 B,我们知道 A,那么我们可以得出 B。

这也被称为 ponens 方式。

和与积

当我们比较总和和乘积的图表时,我们能看到什么?

如果我们翻转箭头,那么它们将是相同的!

这被称为。我们说双轨车的co是我们开始使用的东西。

逻辑中的否定是双重否定的一个例子。

范畴论笑话:

汽车范畴中的态射是什么?自同构

一位牧师、一位拉比和一位自同构走进一家酒吧。。。。“我认为我们应该一对一地谈”,“我们不能……他在骗我们。”

一个绝对数学家怎么称呼椰子?

同构方程

我们学习了以下同构方程:

  • (C,A)x(C,B)=(C,AxB)
  • (A,C)x(C,B)=(A+B,C)
  • (cxa,B)=(C,[A⇒B] )

在第一个等式中,从 C 到 AxB 的箭头是什么意思?

A:我们有一对箭头,从 C 到 A 的箭头和从 C 到 B 的箭头。

2 和 3 的情况也一样。

如果 C 是一个有限集,其中正好有 C 个对象,a 是一个有限集,其中正好有 a 个对象,那么从 C 到 a 有多少种方法?

答:从 C 到 A 有不同的方式

从 n 个变量到 2 有多少个函数?

答:2n

你看到分类同构方程和我们在高中学到的指数定律之间的直接对应关系了吗?

有趣的总和,产品,指数和类型

以下是有老虎和大象的奶牛总数:

这是一个由牛和老虎、大象组成的产品:

以下是奶牛与老虎和大象的对比:

如果我们有一个 getCow 方法可以返回 DressedCows,如果我们有 3 种 DressedCows,那么如果我们调用 getCow,那么它可以返回 3 种可能的 DressedCows。

请注意,没有参数的函数是单位。单元是不携带任何信息的单体类型。在第 9 章函子、单群和泛型中,我们将看到在构建 12 小时时钟函子和编写 reduce 函数时,单位是如何有用的。单位是我们的单位态射。

从代数角度看结构,我们可以找到匹配的结构。

一旦我们确定了同构,我们就已经证明了优化内存使用、性能或数据扩展的方法。

证明我们的代码允许我们使用它。

证明我们的代码不是可以防止错误。

通过这种方式,类型是函数式编程的基本部分。

我们的和与乘积同构。左边的老虎和右边的老虎是的。大象和足球也是如此。老虎的双生体被称为 cotiger。类似地,和被称为余积。

一个单态如下图所示。f单音

一个满态如下图所示。f史诗

下图显示了单态是满态的对偶。

如果我们不丢失从老虎到大象再到老虎的数据:

此外,我们不会丢失从大象到老虎再到大象的数据:

那么,我们的态射是同构的。

代数数据结构使我们在移动、映射和转换数据时确定不会丢失数据。

了解我们的数据结构在工作流程中是如何保存的是至关重要的。

函数式编程为我们带来了数据完整性所需的确定性,以及帮助管理复杂性所需的组合。

当奥比去买铁碗的票时,他输入的是订单数据。。。

当他单击 Submit 按钮时,数据从一个端点流向另一个端点。数据流动时,可能会以某种方式进行转换,但数据结构保持不变:

同构保证数据完整性(无数据丢失)。计算使用数据类型映射态射。接口定义(使用数据类型)允许我们连接函数。不可变的数据结构可以利用内存化,提高并发性,并减少内存使用。在 Go 中使用 FP 可以帮助我们简化开发过程。我们不再需要担心数据/接口不兼容问题的整个分类。

使用 FP 和 Go 的好处包括:

  • 表演
  • 数据可靠性
  • 组件重用
  • 复杂性管理
  • 资源利用

大数据、知识驱动开发和数据可视化

大数据意味着有很多数据。当有大量数据时,很难找到意义。范畴论帮助我们去除不重要的细节,并看到有意义的信息等待被发现。

数据可视化

我们如何将所学知识应用到现实世界中?

构图听起来不错,但我们如何才能做到这一点:

和一个 I/O 单子:

做一些有用的事。

我们可以从服务器日志中读取数据,并集成图形用户界面(GUI),该界面可以呈现一个演示文稿,用户可以查看该演示文稿,并从以有意义的方式呈现的数据中获得理解。

如果我们的数据有相应的模式呢?

我们能否将数据的表示概括为不同的布局?例如,电子表格程序允许其用户基于同一组行和列(饼图、条形图等)显示不同类型的图形。如果我们能够做到这一点,那么以下是可行的:

从实际的角度来看,日志文件中的数据可能会输入到具有行和列的表中,就像电子表格一样。从类别方面来说,我们可以说我们的模式是对象实例及其属性的笛卡尔乘积:模式=实例 x 属性。

然后,我们可以结合我们的图表得到:

现在,如果我们希望允许用户输入查询以获得其领域的工作知识,我们可以得出以下图表:

此处显示的类别定义了一个结构良好的可视化过程;如果箭头、对象和数据是准确的,那么图表将进行转换。我们可以确信,我们获得的知识是可靠的。

知识驱动的系统可以区分成功与失败。我们众所周知的干草堆可以是每一次互动,每一个肢体语言/面部表情的手势,说的话,保持/打破的承诺,等等。如果我们的关键绩效指标是我们关系的健康,那么一个基于知识的系统可以筛选细节并突出一句重要的话,“我希望我到达时你能停止工作并向我打招呼。

有时,我们需要从正确的数据集开始。如果我们的目标是找到下一个建造购物中心的好地方,我们可以收集两个数据集,一个用于所有手机记录,另一个用于人口统计。尽管这两组数据都有大量的数据无法提供指导,但如果我们用规则构建我们的系统,以找到可采取行动的真相,手机记录和人口统计的给定地理位置信息,并结合收入统计,我们的系统可以为我们的投资者提供最理想的场所。

你可能会想,“我可以用我的命令式语言做到这一点。”也许,但它会扩展吗?

总结

在这一章中,你学到了我们需要知道的关于范畴论的知识。我们一起走过历史,学习函数式编程是如何发展到今天的。我们研究了逻辑命题和证明、集合、对象和箭头以及 Lambda 演算。我们对范畴论、逻辑学和 Lambda 演算之间的对应感到惊讶。您还了解了如何将所学知识应用于现实场景(例如足球飞行和与不信任的合作伙伴做生意)。最后,我们深入了解了如何设计基于知识的系统,从大数据中获取价值。

在下一章中,我们将深入研究纯函数式编程。我们将看到如何利用范畴论和类类型来抽象细节,以便收集新的见解。我们将研究函子,以及一些更强大、更有用的函子版本,称为应用函子。我们还将学习如何使用 monads 和 monoids 控制副作用的世界。