滥用代数数据类型的代数 - 为什么这样做有效?

345
"The '代数' 表达式对于具有数学背景的人非常有启发性。让我试着解释一下我的意思。定义了基本类型,如乘积 '•',并集 '+',单例 'X' 和单位 '1'。我们可以使用缩写 'X²' 表示 'X•X',使用 '2X' 表示 'X+X' 等等,然后为链表等定义代数表达式。data List a = Nil | Cons a (List a) ↔ L = 1 + X • L,并且二叉树:data Tree a = Nil | Branch a (Tree a) (Tree a) ↔ T = 1 + X • T²。"
现在,作为一名数学家,我的第一个本能反应是用这些表达式疯狂地做运算,试图解出 LT。我可以通过重复代入来实现这个目标,但是似乎更容易地滥用这个符号,并假装自己可以随意重新排列它。例如,对于一个链表: L = 1 + X • L (1 - X) • L = 1 L = 1 / (1 - X) = 1 + X + X² + X³ + ... 这里,我使用了 1 / (1 - X) 的幂级数展开方式,以一种完全不合理的方式得出了一个有趣的结果,即 L 类型要么是 Nil,要么包含一个元素,要么包含两个元素,或者三个元素等等。
如果我们将其用于二叉树,那就更有趣了: T = 1 + X • T² X • T² - T + 1 = 0 T = (1 - √(1 - 4 • X)) / (2 • X)

T = 1 + X + 2 • X² + 5 • X³ + 14 • X⁴ + ...

再次使用幂级数展开(使用Wolfram Alpha)。这表达了一个对我来说不明显的事实,即只有一个具有1个元素的二叉树,有两个具有两个元素的二叉树(第二个元素可以在左侧或右侧分支上),有五个具有三个元素的二叉树等。

所以我的问题是 - 我在做什么?这些操作似乎没有理由(代数数据类型的平方根到底是什么?),但它们导致了合理的结果。代数数据类型的商在计算机科学中有任何意义,还是只是符号技巧?

而且,更有趣的是,是否可能扩展这些想法?是否存在一种类型代数理论,允许在类型上进行任意函数,还是类型需要幂级数表示?如果你可以定义一类函数,那么函数的组合是否有任何意义?


5
如果它在每个节点中存储数据,那么它的样子要么是 Branch x (Branch y Nil Nil) Nil,要么是 Branch x Nil (Branch y Nil Nil) - Chris Taylor
4
@nlucaroni说"bottom"是一个值而不是一个类型。一个真正的零元类型将没有任何该类型的值,除非你忽略底部值,否则在Haskell中不可能实现。如果考虑底部值,那么仅包含底部的类型成为单元类型,在大多数情况下并不实用,而且很多其他内容也会出错。 - C. A. McCann
3
我同意这是Haskell的常见做法,但仍然觉得有些愚蠢。换句话说,这意味着我们在逻辑和类型理论中与它们不同地使用了“bottom”,这对我来说似乎是不好的。仅仅从纯代码上看起来相同并不意味着它们是相同的:“Tackling the Awkward Squad”明确指出,Haskell的语义具有许多“坏值”,其中永久循环和抛出异常显然不同。用一个代替另一个不是有效的等式推理。Haskell有一套词汇来描述这些糟糕的值“undefined”,“throw”等等。我们应该使用它。 - Philip JF
3
在 Haskell 中,“bottom”这个术语来源于域论,该理论用于定义提升类型的语义。它仍然是一个初始对象,只不过在 Haskell 中我们通常讨论的类别完全不同。 - C. A. McCann
25
这个问题让我大吃一惊。 - TheIronKnuckle
显示剩余21条评论
7个回答

152

免责声明:考虑到⊥,很多内容实际上并不完全正确,因此为了简单起见,我将公然无视它。

一些初始点:

  • 请注意,“union”可能不是A+B的最佳术语——这是两种类型的a disjoint union,因为即使它们的类型相同,两侧也是有区别的。值得一提的是,更常见的术语是“sum type”。

  • 单例类型实际上都是单位类型。它们在代数运算下表现相同,更重要的是,存在的信息量仍然被保留。

  • 您可能还需要一个零类型。Haskell将其提供为Void。没有值的类型为零,就像有一个值的类型为一样。

这里仍然缺少一个主要操作,但我稍后会回来解决。

正如您可能已经注意到的,Haskell倾向于从范畴论中借用概念,上述所有内容都可以非常直观地解释为这样:

  • Hask中给定对象A和B,它们的积A×B是唯一的(同构意义下),它允许两个投影fst:A×B → A和snd:A×B → B,其中给定任何类型C和函数f:C → A,g:C → B,您可以定义配对f &&& g:C → A×B,使得 fst ∘ (f &&& g) = f,并且对于g同理。参数性自动保证了通用特性,而我不那么含蓄的命名选择应该能够说明问题。顺便说一句,(&&&)运算符在Control.Arrow中定义。

  • 上述内容的对偶是余积A+B,其包括injectionsinl:A → A+B和inr:B → A+B。对于给定的任何类型C和函数f:A → C,g:B → C,您可以定义copairingf ||| g:A+B → C,使得明显的等价关系成立。同样,参数性自动保证了大部分复杂的部分。在此情况下,标准的injections简单地是LeftRight,而copairing是函数either

许多关于产品类型和和类型的属性可以从上面推导出来。请注意,任何单例类型都是Hask的终端对象,任何空类型都是初始对象。

回到前面提到的缺失操作,在笛卡尔闭范畴中,你拥有指数对象,它们对应于范畴的箭头。我们的箭头是函数,我们的对象是类型,具有*的种类,并且在类型的代数操作上下文中,类型A -> B的行为确实表现为BA。如果不明显为什么应该保持这种状态,请考虑类型Bool -> A。只有两个可能的输入,该类型的函数与类型A的两个值同构,即(A,A)。对于Maybe Bool -> A,我们有三个可能的输入,依此类推。此外,请注意,如果我们重新用代数符号重述上面的并行定义,我们得到恒等式CA × CB = CA+B

关于为什么这一切都有意义——特别是为什么您使用幂级数展开是合理的——请注意,上述大部分涉及到类型的“居民”(即具有该类型的不同值),以展示其代数行为。为了明确这一观点:
  • 产品类型(A, B)表示从AB中分别取一个值,每个固定的值a :: A都有B的每个元素对应一个(A, B)类型的值。这当然是笛卡尔积,而产品类型的总数是因子数量的乘积。

  • 和类型Either A B表示来自AB的值,左右分支有所区别。正如前面提到的,这是一个不相交的联合,和类型的总数是和式的和。

  • 指数类型B -> A表示从类型B的值到类型A的映射。对于任何固定的参数b :: B,可以将A的任何值分配给它;类型B -> A的值为每个输入选择一个这样的映射,它等价于A的许多副本的乘积,其数量等于B的元素数量,因此是指数。

虽然一开始将类型视为集合似乎很诱人,但在这种情况下并不实用——我们有不相交的联合而非标准的集合并,没有明显的交集或其他许多集合操作的解释,并且我们通常不关心集合成员资格(留给类型检查器处理)。

另一方面,上面的构造花费了大量时间讨论如何“计数”实例,而“枚举”类型的可能值是一个有用的概念。这很快引导我们进入枚举组合学,如果您查阅链接的维基百科文章,您会发现它首先以生成函数的方式定义“对”和“联合”,与产品类型和求和类型完全相同,然后使用完全相同的技术来定义与Haskell列表完全相同的“序列”。


编辑:哦,这里有一个快速的奖励,我认为它能够引人注目地证明这一点。您在评论中提到对于树类型T = 1 + T^2,您可以推导出恒等式T^6 = 1,这显然是错误的。然而,T^7 = T是成立的,并且可以直接构造树和七元组之间的双射,参见Andreas Blass's "Seven Trees in One"

第二次编辑:关于其他答案中提到的“类型导数”构造,您可能还会喜欢同一作者的这篇论文,它进一步建立了这个想法,包括除法和其他有趣的东西。


6
这篇讲解非常棒,特别是作为进入像 http://strictlypositive.org/diff.pdf 这样的内容的起点。 - acfoltzer
31
@acfoltzer:谢谢!:] 是的,这是一篇很好的论文,发展了这些思想。你知道,我认为至少有5%的我的SO总声望可以归功于“帮助人们理解Conor McBride的一篇论文”... - C. A. McCann
关于T^7案例的问题 - 那篇论文特别描述了一个“非常明确”,“特别基础”的双射,但为什么这些限定词对这些等式有影响呢?换句话说,T和T^n(其中n是常数)都是可数无穷的,因此它们之间存在双射,那么为什么我们不能说T=T^2=T^3=T^4=…等等? - Christopher Shroba

53
二叉树由方程式T=1+XT^2在类型半环中定义。通过构造,T=(1-sqrt(1-4X))/(2X)也由相同的方程式在复数半环中定义。因此,考虑到我们在相同的代数结构类中解决相同的方程式,我们实际上看到了一些相似之处并不奇怪。
问题在于,当我们在复数半环中推理关于多项式的内容时,我们通常使用复数形成环甚至是域的事实,因此我们发现自己在使用减法等不适用于半环的运算。但是,如果我们有一个规则,允许我们从等式两侧消除,则可以经常从我们的论据中消除减法。这是Fiore and Leinster所证明的,在半环中许多关于环的论点可以转移到半环。
这意味着您关于环的许多数学知识可以可靠地转移到类型。因此,某些涉及复数或幂级数(在形式幂级数环中)的论据可以以完全严格的方式转移到类型中。
然而,这个故事的背后还有更多。证明两种类型相等(例如通过证明两个幂级数相等)是一回事,但是通过检查幂级数中的项,您也可以推断出关于类型的信息。我不确定这里的正式陈述应该是什么。(我建议参考 Brent Yorgey 的 paper 关于组合物种的一些相关工作,但是物种与类型并不相同。)
我发现令人惊叹的是,您所发现的内容可以扩展到微积分学中。微积分学的定理可以转移到类型的半环上。实际上,甚至可以将有限差分的论证转移到类型理论中,并且您会发现数值分析中的经典定理在类型理论中有解释。
玩得开心!

这个差异化/单孔上下文的东西很酷。让我们看看我是否理解正确。一个代数表示为 P = X^2 的对,有导数 dP = X + X,所以 Either 是一孔上下文的对。这很酷。我们也可以“积分” Either 来得到一个对。但是如果我们尝试“积分” Maybe(类型为 M = 1 + X),那么我们需要 \int M = X + X^2 / 2,这是荒谬的(什么是半个类型?)。这是否意味着 Maybe 不是任何其他类型的单孔上下文? - Chris Taylor
6
@ChrisTaylor说:单孔上下文可以保留有关产品内部位置的信息,即带有一个孔的(A,A)代表一个A和一位告诉你孔在哪一侧的位。一个单独的A没有区别化的孔可填充,这就是为什么你不能“集成”它的原因。在这种情况下缺失信息的类型显然是2 - C. A. McCann
我写过关于理解像 X^2/2 这样的类型的文章:http://blog.sigfpe.com/2007/09/type-of-distinct-pairs.html - sigfpe
@user207442,你不是也做过一棵树和七棵树之间的双射吗?我在我的回答中链接了一篇关于这个问题的论文,但我敢打赌我最初是在你的博客上读到它的。 - C. A. McCann
1
@ChrisTaylor 关于有限(实际上是“分割”)差异,可以参考这篇文章:http://strictlypositive.org/CJ.pdf 但在那时 Conor 没有意识到他正在描述差异。我写了这篇文章,虽然可能有点棘手:http://blog.sigfpe.com/2010/08/divided-differences-and-tomography-of.html 我会写一篇论文,但我不太擅长完成它们。 - sigfpe
显示剩余2条评论

24

看起来你所做的只是扩展循环关系。

L = 1 + X • L
L = 1 + X • (1 + X • (1 + X • (1 + X • ...)))
  = 1 + X + X^2 + X^3 + X^4 ...

T = 1 + X • T^2
L = 1 + X • (1 + X • (1 + X • (1 + X • ...^2)^2)^2)^2
  = 1 + X + 2 • X^2 + 5 • X^3 + 14 • X^4 + ...

由于类型操作的规则与算术运算的规则相似,因此您可以使用代数方法来帮助您计算如何扩展递归关系(因为这并不明显)。


1
由于类型操作的规则与算术操作的规则类似,因此......但实际上并不是这样。没有类型的减法概念,更不用说除法和平方根了。所以我的问题是:当你假设X是实数元素进行代数运算时,你什么时候可以得出关于类型的真实陈述?此外,系数(第n次项)<=>(持有n个元素的类型数量)的对应关系从何而来? - Chris Taylor
1
例如,从树的表达式(T = 1 + T^2)中,我可以推导出 T^6 = 1 (即解方程 x^2 - x + 1 = 0 的解是六次单位根),但显然不能将由六个二叉树组成的乘积类型等同于单元 () - Chris Taylor
4
@ChrisTaylor,但在那里有一些事情正在发生,因为T^7T之间存在同构关系。参考网址:http://arxiv.org/abs/math/9405205 - luqui
8
@ChrisTaylor,这是一个值得思考的问题。当你添加新的代数运算时,希望不会破坏现有代数运算的性质。如果你可以用两种不同的方式得出相同的答案,那么它们应该是相同的。因此,只要对于L = 1 + X * L存在任何表示方式,为了保持一致,最好与通过级数展开得到的表示方式相同。否则,你可能会逆推回到关于实数的错误结果。 - luqui
2
@ChrisTaylor,确实有类型的分割概念,请搜索“商类型”以获取更多信息。它是否与多项式除法相对应,我不知道。在我看来,它相当不切实际,但确实存在。 - Doug McClean

24

微积分和麦克劳林级数类型

这里是另一个小的补充 - 对于为什么级数展开中的系数应该“有效”的组合洞察力,特别关注可以使用微积分中的Taylor-Maclaurin方法导出的级数。注意:您提供的操作列表类型的示例级数展开是麦克劳林级数。

由于其他答案和评论涉及代数类型表达式(求和、乘积和指数)的行为,因此本答案将省略该细节并专注于类型“微积分”。

您可能会注意到本答案中的引号正在进行一些重要工作。原因有两个:

  • 我们从一个领域给出解释到另一个领域的实体,似乎适当地用这种方式限定这样的外来概念。
  • 一些概念将能够更严谨地形式化,但形状和思想似乎更重要(并且需要更少的空间来编写)比细节。

麦克劳林级数的定义

函数 f: ℝ → ℝ麦克劳林级数定义为:

f(0) + f'(0)X + (1/2)f''(0)X² + ... + (1/n!)f⁽ⁿ⁾(0)Xⁿ + ...

其中f⁽ⁿ⁾表示f的第n阶导数。

为了能够理解用类型解释的Maclaurin级数,我们需要了解如何在类型上下文中解释三个概念:

  • 一个(可能是多个)导数
  • 将函数应用于0
  • 类似于(1/n!)的术语

事实证明,这些来自分析学的概念在类型世界中有合适的对应物。

我所说的“合适的对应物”是什么意思?它应该具有同构的特点——如果我们可以在两个方向上保持真实性,那么在一个上下文中可推导出的事实可以转移到另一个上下文中。

带类型的微积分

那么类型表达式的导数是什么意思?事实证明,对于一大类良好行为(“可微分”)的类型表达式和函子,存在一种自然操作,其行为足够相似,以便成为合适的解释!

为了不泄漏结论,类比微分的操作是制作“单孔上下文”。 这里 是一个扩展这个特定点的绝佳场所,但一个单孔上下文(da/dx)的基本概念是它表示从术语(类型为a)中提取特定类型的单个子项(x)的结果,保留所有其他信息,包括确定子项原始位置所需的信息。例如,表示列表的单孔上下文的一种方法是使用两个列表:一个用于在提取之前的项目,另一个用于在提取之后的项目。
将此操作与微分进行类比的动机来自以下观察结果。我们写da/dx表示具有类型a和类型x孔的单孔上下文的类型。
d1/dx = 0
dx/dx = 1
d(a + b)/dx = da/dx + db/dx
d(a × b)/dx = a × db/dx + b × da/dx
d(g(f(x))/dx = d(g(y))/dy[f(x)/a] × df(x)/dx

在这里,10分别表示具有恰好一个和恰好零个成员的类型,+×像往常一样表示求和和乘积类型。 fg用于表示类型函数或类型表达式形成器,[f(x)/a]表示将f(x)替换为前面表达式中的每个a的操作。

这可以用无点风格来写,用f'表示类型函数f的导数函数,因此:

(x ↦ 1)' = x ↦ 0
(x ↦ x)' = x ↦ 1
(f + g)' = f' + g'
(f × g)' = f × g' + g × f'
(g ∘ f)' = (g' ∘ f) × f'

这可能更可取。

请注意,如果我们使用类型和函子的同构类定义导数,则可以使等式严格而精确。

现在,我们特别注意到微积分中关于加法、乘法和组合的代数运算规则(通常称为求和、乘积和链式法则)恰好反映了“挖一个洞”的操作。此外,“挖一个洞”在常量表达式或项x本身中的基本情况也表现为微分,因此通过归纳,我们可以得到所有代数类型表达式的类似微分的行为。

现在我们可以解释微分,dⁿe/dxⁿ表示类型表达式的第n个“导数”是什么意思?它是表示n个位置上下文的类型:当用n个类型为x的术语“填充”时,会产生一个e。还有另一个与后面的'(1/n!)'相关的关键观察。

类型函子的不变部分:将函数应用于0

在类型世界中,我们已经对0有了一个解释:一个没有成员的空类型。从组合的角度来看,将类型函数应用于它意味着什么?更具体地说,假设f是一个类型函数,那么f(0)是什么样子?显然,我们没有访问任何类型为0的东西,因此任何需要xf(x)构造都不可用。剩下的是那些在它们的缺席中可以访问的术语,我们可以称之为类型的“不变”或“常量”部分。

以明确的例子为例,取Maybe函子,可以代数地表示为x ↦ 1 + x。当我们将其应用于0时,我们得到1 + 0——就像1一样:唯一可能的值是None值。类似地,对于列表,我们只得到对应于空列表的项。

当我们将其带回并将类型f(0)解释为一个数字时,它可以被视为没有访问到x的情况下,可以获得多少个类型f(x)(对于任何x)的项的计数:也就是说,“空类似”项的数量。

将其整合在一起:Maclaurin级数的完整解释

恐怕我无法想出一个适当的直接解释(1/n!)作为一种类型。

如果我们考虑上面提到的类型f⁽ⁿ⁾(0),我们可以将其解释为类型f(x)n个位置上下文,这些上下文不包含x - 也就是说,当我们对它们进行n次“积分”时,得到的结果恰好有恰好nx,没有多余或缺少。那么,将类型f⁽ⁿ⁾(0)解释为数字(例如f的Maclaurin级数的系数)只是计算这样的空的n个位置上下文的数量。我们已经接近成功了!

但是,(1/n!)在哪里呢?检查类型“微分”的过程会告诉我们,当应用多次时,它保留了提取子项的“顺序”。例如,考虑类型为x × x的术语(x₀, x₁)和在其中两次“挖洞”的操作。我们得到了两个序列:

(x₀, x₁)  ↝  (_₀, x₁)  ↝  (_₀, _₁)
(x₀, x₁)  ↝  (x₀, _₀)  ↝  (_₁, _₀)
(where _ represents a 'hole')

即使两者来自同一术语,因为有 2!= 2 种方法可以从两个元素中取两个,保留顺序。通常,从 n 个元素中取出 n! 种方法。因此,为了得到一个函子类型的配置数,其具有n个元素,我们必须计算类型f⁽ⁿ⁾(0)并除以n!正如Maclaurin级数的系数一样。

因此,除以n!结果被解释为它本身。

最终思考:'递归'定义和解析性

首先,一些观察:

  • 如果函数 f:ℝ→ℝ 有导数,则此导数是唯一的
  • 同样,如果函数 f:ℝ→ℝ 是解析的,则它具有唯一对应的多项式级数

由于我们有链式法则,如果我们将类型导数形式化为同构类,则可以使用隐式微分。但是隐式微分不需要任何外来的操作,如减法或除法!因此,我们可以使用它来分析递归类型定义。以您的列表示例为例,我们有

L(X) ≅ 1 + X × L(X)
L'(X) = X × L'(X) + L(X)

然后我们可以进行评估

L'(0) = L(0) = 1

获取 Maclaurin 级数中 的系数。

但是,由于我们确信这些表达式确实是严格“可微”的(即使只是隐式的),并且由于我们与函数 ℝ → ℝ 的对应关系,在那里导数肯定是唯一的,因此我们可以放心地说,即使我们使用“非法”操作获得值,结果也是有效的。

现在,类似地,为了使用第二个观察结果,由于与函数 ℝ → ℝ 的对应关系(它是同态吗?),我们知道,只要我们满意一个函数 Maclaurin 级数,如果我们能找到任何级数,上述原则就可以应用于使其严谨。

至于你关于函数组合的问题,我想链式法则提供了部分答案。

我不确定这适用于多少个 Haskell 风格的 ADT,但我怀疑它是许多甚至全部。我已经发现了一个真正奇妙的证明,但这个边距太小了,无法容纳它...

现在,当然,这只是解决这里正在发生的事情的一种方式,可能还有许多其他方式。

总结:TL;DR

  • 'differentiation' 类型对应于 '挖一个洞'。
  • 将函子应用于 0 可以得到该函子的 "空类" 术语。
  • Maclaurin 幂级数因此(在某种程度上)严谨地对应于枚举具有特定元素数量的函子类型的成员数量。
  • 隐式求导 使这更加严密。
  • 导数的唯一性和幂级数的唯一性意味着我们可以拿捏细节,而它仍然有效。

如果X和Y是向量空间,f:X -> Y是一个函数,则导数的“类型”为Df:X -> L(X,Y),其中L(X,Y)是从X到Y的线性变换空间。另请参见https://en.wikipedia.org/wiki/Fr%C3%A9chet_derivative。 - Keith A. Lewis

19

12
“拉链技巧”非常棒。我希望能够理解它。 - spraff
您还可以使用分界限延续在Scheme中创建拉链,这使您能够从它们中泛化派生。 - Jon Purdy

10
ACP(Communicating Processes的代数)处理与进程相关的类似表达式。它提供加法和乘法作为选择和序列的运算符,并带有相关的中性元素。基于这些,可以使用其他构造的运算符,如并行和干扰。详见http://en.wikipedia.org/wiki/Algebra_of_Communicating_Processes。此外,还有一篇名为“进程代数简史”的在线论文。
我正在研究将ACP与编程语言扩展。去年四月,我在Scala Days 2012上发表了一篇研究论文,可在http://code.google.com/p/subscript/找到。
在会议上,我演示了一个并行递归包规范的调试器:
Bag = A; (Bag & a)
其中,A和a代表输入和输出操作;分号和&代表序列和并行。请参见从前面链接可以访问的SkillsMatter视频。
与L = 1 + X•L更相似的包规范是
B = 1 + X&B
ACP在使用公理时是以选择和序列来定义并行的;请参见维基百科文章。我想知道L = 1 / (1-X)的包比喻是什么?
ACP风格的编程对于文本解析器和GUI控制器非常方便。例如,可以通过使两个细化(如Scala允许函数)的“clicked”和“key”隐式来更简洁地编写以下规范:
searchCommand = clicked(searchButton) + key(Enter)
cancelCommand = clicked(cancelButton) + key(Escape)
因此,我们可以写成:
searchCommand = searchButton + Enter cancelCommand = cancelButton + Escape

右侧现在包含的是数据操作数,而不是进程。在这个层次上,不需要知道隐式细化将如何将这些操作数转换为进程;它们不一定会细化成输入操作;输出操作也适用,例如在测试机器人的规范中。

进程会以数据为伴侣变得这样;因此我创造了术语“项代数”。


10

依赖类型理论和“任意”类型函数

我对这个问题的第一个回答充满了概念,缺乏细节,并反映了子问题:“发生了什么?”;这个答案也是一样的,但专注于子问题:“我们能得到任意类型的函数吗?”。

对于代数运算中的和与积,有一个扩展叫做“大型运算符”,它表示一个序列(或更一般地,域上的一个函数)的和与积,通常分别写作ΣΠ。请参见Sigma Notation

因此,总和为

a₀ + a₁X + a₂X² + ...

可能会被写成
Σ[i ∈ ℕ]aᵢXⁱ

其中a是一些实数序列,例如。乘积将类似地表示为Π而不是Σ

当您从远处观看时,这种表达式看起来很像X中的“任意”函数; 当然,我们受限于可表达的级数及其相关的解析函数。这是否是类型理论的一种表示候选?绝对是!

具有这些表达式的直接表示的类型理论类是“依赖”类型理论:具有依赖类型的理论。自然地,我们有依赖于术语的术语,并且在像Haskell这样具有类型函数和类型量化的语言中,还有依赖于类型的术语和类型。在依赖设置中,我们还有依赖于术语的类型。 Haskell不是依赖类型的语言,尽管可以通过折磨语言来模拟许多依赖类型的特性 (链接)

Curry-Howard和依赖类型

“Curry-Howard同构”最初是一种观察,即简单类型λ演算的术语和类型判断规则与直觉命题逻辑(由Gentzen制定)应用于自然演绎完全对应,其中类型代替命题,术语代替证明,尽管两者是独立发明/发现的。从那时起,它一直是类型理论家的巨大灵感源泉。最明显的考虑之一是这种命题逻辑的对应是否可以扩展到谓词或高阶逻辑,以及如何扩展。依赖类型理论最初就是从这个探索方向出现的。
有关简单类型λ演算的Curry-Howard同构的介绍,请参见此处。例如,如果我们想要证明A ∧ B,我们必须证明A并证明B;联合证明只是一对证明:每个连结词都有一个。
在自然演绎中:
Γ ⊢ A    Γ ⊢ B
Γ ⊢ A ∧ B

在简单类型 λ 演算中:
Γ ⊢ a : A    Γ ⊢ b : B
Γ ⊢ (a, b) : A × B

类似的对应关系也存在于 和和类型、 和函数类型,以及各种消解规则上。
一个不可证明的(直觉上为假的)命题对应于一个无人居住的类型。
考虑到类型与逻辑命题之间的类比,我们可以开始思考如何在类型世界中建模谓词。有许多方式进行形式化(参见 Martin-Löf 直觉型理论的 这个介绍 作为广泛使用的标准),但抽象方法通常观察到,谓词就像具有自由项变量的命题,或者说是将项映射到命题的函数。如果我们允许类型表达式包含项,则 lambda 演算风格的处理方法立即呈现出一种可能性!

考虑仅有建设性证明,那么构成 ∀x ∈ X.P(x) 的证明是什么?我们可以将其视为一个证明函数,将项 (x) 映射到相应命题的证明 (P(x))。因此,类型 (命题) ∀x : X.P(x) 的成员(证明)是“依赖函数”,对于每个 X 中的 x,它们给出类型为 P(x) 的项。

那么 ∃x ∈ X.P(x) 呢?我们需要任何 X 的成员 x,以及一个证明 P(x)。因此,类型 (命题) ∃x : X.P(x) 的成员(证明)是“依赖对”:一个特殊的 X 中的项 x,加上类型为 P(x) 的项。

符号: 我将使用

∀x ∈ X...

关于类X成员的实际声明,以及
∀x : X...

针对类型表达式的全称量词,相应地使用 X。同样适用于
组合学考虑:积和和
除了类型与命题的柯里-霍华德对应关系外,我们还有代数类型与数字和函数的组合学对应关系,这是本问题的重点。令人高兴的是,这可以扩展到上述依赖类型!
我将使用模数符号。
|A|

“size”是指代类型A的大小,明确了问题中类型和数字之间的对应关系。请注意,这是理论之外的概念;我并不认为语言内需要有这样的运算符。
让我们来数一下该类型的可能成员(完全约简、规范化的)。
∀x : X.P(x)

这是一种依赖函数类型,它将类型为X的术语x转换为类型为P(x)的术语。每个这样的函数必须为X的每个术语提供输出,并且此输出必须是特定类型的。对于X中的每个x,则会给出|P(x)|个输出“选择”。
重点是:
|∀x : X.P(x)| = Π[x : X]|P(x)|

这当然对于类型为XIO ()的情况没有太大意义,但适用于代数类型。
同样地,一个类型为的术语
∃x : X.P(x)

这是一种由带有 p: P(x) 的配对 (x, p) 组成的类型,因此对于集合 X 中的任何元素 x,我们可以构造出一个适当的配对,其中包含 P(x) 中的任何成员,从而给出 |P(x)| 种“选择”。
因此,
|∃x : X.P(x)| = Σ[x : X]|P(x)|

“带有相同警告”的意思是相同的限制条件或注意事项。
这证明了在使用符号Π和Σ的理论中,依赖类型的常见表示法,并且确实许多理论模糊了“对于所有”和“乘积”以及“存在”和“总和”之间的区别,因为上述对应关系。
我们快要接近了!
向量:表示依赖元组
现在我们能否编码数值表达式,例如
Σ[n ∈ ℕ]Xⁿ

作为类型表达式吗?
不完全是。虽然我们可以非正式地考虑在 Haskell 中的表达式的含义,例如 Xⁿ,其中 X 是一个类型,n 是一个自然数,但这是一种滥用符号的方式;这是包含数字的类型表达式:明显不是有效的表达式。
另一方面,有了依赖类型的概念,包含数字的类型恰恰是重点;事实上,依赖元组或“向量”是如何提供 类似于列表访问的实用类型级安全性 的常见示例。向量只是列表以及关于其长度的类型级信息:这正是我们想要的类型表达式,例如 Xⁿ
在本答案的持续时间中,让我们将其定义为:
Vec X n

这是一种长度为 n 的 X 类型值向量。在技术上,n 并不是一个实际的自然数,而是自然数系统中的一个表示。我们可以用 Peano 风格来表示自然数(Nat),其中零(0)或另一个自然数的后继(S)。对于 n ∈ ℕ,我用 ˻n˼ 来表示代表 n 的 Nat 项。例如,˻3˼ 是 S (S (S 0))。
因此,我们有
|Vec X ˻n˼| = |X|ⁿ

对于任何自然数 n ∈ ℕ

Nat 类型:将 ℕ 项提升为类型

现在我们可以编码像这样的表达式:

Σ[n ∈ ℕ]Xⁿ

类型。这个特定的表达式会产生一个类型,当然是同构于问题中所识别的X列表类型。(不仅如此,从范畴论的角度来看,将X带入上述类型的类型函数 - 即一个函子 - 是与List函子自然地同构的。)
对于“任意”函数的最后一块拼图是如何编码的。
f : ℕ → ℕ

expressions like

Σ[n ∈ ℕ]f(n)Xⁿ

为了能够将任意系数应用于幂级数。
我们已经理解了代数类型与数字之间的对应关系,使我们能够从类型映射到数字和类型函数映射到数字函数。我们还可以反过来! - 给定一个自然数,无论是否有依赖类型,显然都有一个可定义的具有那么多项成员的代数类型。我们可以在类型理论之外轻松地通过归纳证明这一点。我们需要的是一种从自然数到类型的映射方法,在系统内部。
一个令人愉悦的认识是,一旦我们拥有了依赖类型,归纳证明和递归构造变得密切相似 - 在许多理论中它们实际上是完全相同的东西。既然我们可以通过归纳证明存在满足我们需求的类型,那么我们不应该能够构造它们吗?
在术语层面上表示类型的几种方法。我将使用一个想象中的 Haskell 风格符号,用“*”表示类型宇宙,本身通常在依赖设置中被认为是一种类型。
同样,表示'ℕ-消除'的记法至少有与依赖类型理论相同的数量。我将使用 Haskell 风格的模式匹配符号。
我们需要一个映射,从Nat到*的α,满足以下条件。
∀n ∈ ℕ.|α ˻n˼| = n.

以下的伪定义足以说明。
data Zero -- empty type
data Successor a = Z | Suc a -- Successor ≅ Maybe

α : Nat -> *
α 0 = Zero
α (S n) = Successor (α n)

因此,我们看到α的操作镜像了后继函数S的行为,使其成为一种同态。 Successor是一种类型函数,它将一个类型的成员数加1; 也就是说,对于任何定义大小的a| Successor a | = 1 + | a |
例如,α˻4˼(即α(S(S(S(S 0)))))是:
Successor (Successor (Successor (Successor Zero)))

这种类型的条款是
Z
Suc Z
Suc (Suc Z)
Suc (Suc (Suc Z))

给出了四个元素,确切地说:|α ˻4˼| = 4

同样地,对于任意的自然数n ∈ ℕ,我们有

|α ˻n˼| = n

根据需要。
许多理论要求*的成员仅是类型的代表,并且提供了一个操作作为从类型*的项显式映射到它们关联类型的映射。其他理论允许字面上的类型本身成为项级实体。
“任意”函数?
现在我们有了表达完全一般的幂级数作为类型的工具!
该级数
Σ[n ∈ ℕ]f(n)Xⁿ

成为类型
∃n : Nat.α (˻f˼ n) × (Vec X n)

其中˻f˼: Nat → Nat是函数f在该语言中的适当表示。我们可以理解为:
|∃n : Nat.α (˻f˼ n) × (Vec X n)|
    = Σ[n : Nat]|α (˻f˼ n) × (Vec X n)|          (property of ∃ types)
    = Σ[n ∈ ℕ]|α (˻f˼ ˻n˼) × (Vec X ˻n˼)|        (switching Nat for ℕ)
    = Σ[n ∈ ℕ]|α ˻f(n)˼ × (Vec X ˻n˼)|           (applying ˻f˼ to ˻n˼)
    = Σ[n ∈ ℕ]|α ˻f(n)˼||Vec X ˻n˼|              (splitting product)
    = Σ[n ∈ ℕ]f(n)|X|ⁿ                           (properties of α and Vec)

这到底有多“任意”?通过这种方法,我们不仅受限于整数系数,还受限于自然数。除此之外,对于给定的Turing Complete依赖类型语言,f可以是任何函数,我们可以用自然数系数来表示任何解析函数。
我还没有研究过这个与问题中提供的List X ≅ 1/(1 - X)的交互作用,或者在这种情况下这些负数和非整数“类型”可能具有什么可能的意义。
希望这个答案能够探讨我们可以通过任意类型函数走多远。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接