什么是行类型?它们是否属于代数数据类型?

35

我经常听说 F# 不支持 OCaml 行类型,这使得 OCaml 比 F# 更加强大。

它们是什么?它们是代数数据类型,例如和类型(discriminated unions)或积类型(元组,记录)吗?在其他方言中,比如 F# 中是否可以编写行类型?


它们是什么:https://caml.inria.fr/pub/docs/manual-ocaml/extn.html#s-private-rows - Pierre G.
2
@PierreG. 这是关于 private 关键字在行类型情况下的行为描述,而不是关于行类型本身的描述。 - PatJ
3
我不熟悉OCaml,但行多态性是记录类型与子类型的一种替代方案。行多态性允许您在行变量中保留“未使用”的类型信息(记录结构的一部分),这些数据类型在行变量中具有多态性。然而,使用子类型会导致丢失这些类型信息。 - user6445533
1
@AndreyTyukin,我看到你为这个问题添加了悬赏。是否有什么特定的东西是现有答案中缺失的,你感兴趣的? - Tomas Petricek
@TomasPetricek 不是这样的。就像悬赏信息所说的那样,它存在是因为我发现其中一个答案非常优秀,值得获得额外的悬赏金。我不能立即授予它,系统告诉我要等待24小时。 - Andrey Tyukin
2
@AndreyTyukin 啊,我错过了这条消息。我完全同意,现有的答案非常详尽,值得额外的赞扬! - Tomas Petricek
3个回答

55
首先,让我们确保我们使用的术语与OCaml类型系统和相应的白皮书保持一致。在OCaml的类型系统中,并不存在所谓的“行类型”,然而,它具有“行多态性”,我们将在下面讨论它0
行多态性是一种多态性形式。OCaml提供了两种多态性 - 参数化和行多态性,并且缺少另外两种 - 特设和包含(也称为子类型)1
首先,什么是多态性?在类型系统的背景下,多态性允许一个术语具有多个类型。问题在于,在计算机科学和编程语言社区中,术语“类型”本身就有很多含义。为了减少混淆,让我们在这里重新介绍一下,以确保大家在同一页面上。术语的类型通常表示术语语义的某种近似。语义可以是简单的一组带有一组操作的值,也可以是更复杂的东西,比如效果、注释和任意理论。总的来说,语义表示一个术语的所有可能行为的集合。类型系统表示一组规则,根据它们的类型,允许某些语言结构并禁止其他语言结构。也就是说,它验证术语的组合是否正确行为。例如,如果语言中有一个函数应用结构,类型系统将仅允许将应用应用于具有与参数类型匹配的类型的参数。这就是多态性发挥作用的地方。在单态类型系统中,这种匹配只能是一对一的,即字面意义上的。多态类型系统提供了一些机制,用于指定将与一组类型匹配的正则表达式。因此,不同类型的多态性只是您可以用来表示类型族的不同类型的正则表达式。
现在让我们从这个角度来看不同种类的多态性。例如,参数多态性就像正则表达式中的点。例如,'a list. list - 这意味着我们与 list 字面匹配,而 list 类型的参数可以是任何类型。行多态性是一个星号操作符,例如,<quacks : unit; ..><quacks : unit; .*> 是相同的。它意味着它与任何具有 quacks 属性的类型匹配,并执行其他操作。谈到名义子类型,这种情况下我们有名义类(也称为正则表达式中的字符类),并且我们使用它们的基类的名称来指定一组类型。例如,duck 就像 [:duck:],任何被正确“注册”为该类成员的值都与该类型匹配(通过类继承和 new 操作符)。最后,特设多态性实际上也是名义的,并且映射到正则表达式中的字符类。这里的主要区别在于特设多态性中的类型概念不是应用于值,而是应用于名称。因此,一个名称,比如函数名或者 + 运算符,可能有多个定义(实现),这些定义应该使用某种语言机制进行静态注册(例如,重载运算符,实现方法等)。因此,特设多态性只是名义子类型的一种特殊情况。
现在,当我们清楚了,我们可以讨论一下行多态性给我们带来了什么。行多态性是结构类型系统的一个特性(在动态类型语言中也被称为鸭子类型),与名义类型系统相对应,名义类型系统提供了子类型多态性。总的来说,正如我们上面讨论的那样,它允许我们将类型指定为“任何会嘎嘎叫的东西”,而不是“任何实现IDuck接口的东西”。当然,你可以通过定义鸭子接口并显式地将所有实现注册为该接口的实例来使用名义类型,使用一些inheritimplements机制。但是这里的主要问题是你的继承关系是封闭的,也就是说,你需要改变你的代码来在新创建的接口中注册一个实现。这违反了开闭原则并阻碍了代码的重用。名义子类型的另一个问题是,除非你的继承关系形成一个格子(即对于任意两个类,总是存在一个最小上界),否则你无法对其进行类型推断5

进一步阅读

  • Objective ML:一种有效的面向对象的ML扩展 - 对该主题的全面描述。

  • François Pottier和Didier Rémy。ML类型推断的本质。在Benjamin C. Pierce(编辑),《类型和编程语言的高级主题》,MIT出版社,2005年。 - 请参阅第10.8节,了解关于行的非常彻底和详细的解释。

  • 结构多态的简单类型推断 - 对在类型推断存在的情况下,结构和行多态之间相互作用的详细解释。

---- 0)正如@nekketsuuu在评论中指出的那样,我在使用术语时有些主观,因为我的意图是给出一个易于理解和高层次的概念,而不深入细节。自那时以来,我已经修订了帖子,使其稍微更加严格。

1) 尽管OCaml提供了具有继承和子类型概念的类,但根据通常的定义,它并不是一种子类型多态,因为它不是名义上的。从回答的其余部分可以更清楚地看出这一点。

2) 我只是在修正术语,我并不声称我的定义是正确的。许多人认为类型表示值的表示,从历史上看这是正确的。

3) 也许更好的正则表达式应该是<.*; quacks : unit; .*>,但我认为你已经理解了意思。

4) 因此,尽管OCaml具有子类型的概念,但它没有子类型多态。当您指定一个类型时,它不会与子类型匹配,它只会字面匹配,并且您需要使用显式的向上转型运算符将类型为T的值应用于期望super(T)的上下文中。因此,尽管OCaml中存在子类型,但它与多态无关。

5) 虽然格子要求看起来并不难,但在现实生活中,对层次结构施加这种限制很难,或者如果施加了这种限制,类型推断的精确性将始终与类型层次的精确性绑定在一起。所以在实践中,它行不通,参见Scala。

(第一次阅读时请跳过此注释)尽管在OCaml中存在用于将行多态嵌入OCaml类型推断的行变量,但OCaml类型推断仅具有参数多态。

‡) 通常,单词typing与类型系统可以互换使用,用于指代整体类型系统中的一组特定规则。例如,有时我们说"OCaml具有行类型"来表示OCaml类型系统提供了"行多态"的规则。


3
非常详细的回答 - 特别是层次结构中的格子结构非常有启发性!然而,我在“因此,临时多态性只是名义子类型化的一种特殊情况”方面无法理解您的意思。我将临时多态性描绘为两种类型在特定目的下等效。虽然我看不到这两种类型之间的子类型关系。 - user6445533
2
是的,我试图简洁明了,因为我不想深入讨论特定的即兴类型,这是完全不同的问题。即兴多态类型表示一组类型,这些类型通过将自己(即通过绑定到某个名称,而不是通过结构)放置在适用于此名称的类别中来进行标记。这基本上是名义子类型的特例,只是接口是通过重载函数的名称指定的,并且层次高度为一。 - ivg
例如,让我们考虑C ++。它不提供一种机制来定义一个特定多态函数的接口,而不需要至少一个实现(类似于CLOS中的defgeneric)。因此,当我们定义void f(int x) {...}时,我们隐含地定义了一个接口f(x),这使得所有实现方法f的值,例如void f(float x) {...}成为函数f作为参数接受的类型族的一部分。顺便说一句,CLOS是一个很好的例子,其中名义子类型实际上是通过重载实现的。 - ivg
@ivg 精确地说,行多态和子类型是不同的概念。请参见 https://cs.stackexchange.com/q/53998/58774 或者 这篇文章 - nekketsuuu
@ivg 很好的回答,但我对这个语句感到困惑:“但主要问题在于你的层次结构是sealed的,即你需要改变你的代码以在一个新创建的接口中注册一个实现。”为什么会这样?任何针对duck接口编写的逻辑都可以继续工作,而不需要修改包含该逻辑的代码。duck的新实现可以在另一个单元构建,比如库中实现。我看不到这种情况下有什么被封闭的东西。相反,加入新类型的便利性不是命名子类型的重点吗? - GrumpyRodriguez
显示剩余6条评论

16

行类型很奇怪,但非常强大。

行类型用于在OCaml中实现对象和多态变体。

但首先,这里是没有行类型情况下我们无法做到的事情:

type t1 = { a : int; b : string; }
type t2 = { a : int; c : bool; }

let print_a x = print_int x.a

let ab = { a = 42; b = "foo"; }
let ac = { a = 123; c = false; }

let () =
 print_a ab;
 print_a ac

当然,这段代码不会编译,因为print_a必须具有唯一类型:要么是t1,要么是t2,但不能同时具有。然而,在某些情况下,我们可能希望出现这种行为。这就是行类型的作用。它们的作用是提供更加“灵活”的类型。

在OCaml中,行类型有两个主要用途:对象多态变体。从代数角度来看,对象给你“行乘积”,而多态变体则给你“行总和”。

需要注意的是,行类型可能导致一些子类型声明,并且在类型和语义上非常反直觉(特别是在类的情况下)。有关详细信息,请参见本文


7
你的回答一开始很有希望,但很快就结束了。你基本上只给了两个链接。好吧,三个链接。:( - Will Ness
1
我认为行多态性并没有什么奇怪的地方。此外,它是最自然和直观的,这就是鸭子类型,并且被Python和JavaScript程序员愉快地使用,毫无疑问。我认为这是人们首先想到的多态性形式。 - ivg
@ivg,那只是一种幽默的注释而已。在动态类型行多态性方面,确实会想到行类型,但从OCaml世界的其他部分来看,行类型对我来说似乎很奇怪。我的意思是,广义代数数据类型和模块比行多态性更适合我。我想这可能是你思考方式的问题。 - PatJ
@ivg 函数对象有点作弊,你必须为你的类型命名。我认为临时部分真的很烦人。实际上,所有关于对象、类和(在较小程度上)多态变量的语义都让我感到困扰。 - PatJ
不必命名您的类型(我知道名称是程序员最有价值的资产) 。您只需编写 module F(A : sig type t end) 即可。此外,对象、类和多态变量并非特别的。到目前为止,在OCaml中还没有特别的多态性,直到添加模块隐式参数。 - ivg
显示剩余2条评论

9
我将使用中文进行翻译,以下是需要翻译的内容:

我将使用类对PatJ的优秀答案进行补充,他的例子已经用类写出。

给定以下类:

class t1 = object
  method a = 42
  method b = "Hello world"
end

class t2 = object
  method a = 1337
  method b = false
end

以下是对象:

let o1 = new t1
let o2 = new t2

你可以编写以下内容:
let print_a t = print_int t#a;;
val print_a : < a : int; .. > -> unit = <fun>

print_a o1;;

42
- : unit = ()

print_a o2;;

1337
- : unit = ()

你可以在print_a的签名中看到行类型。 < a : int; .. >是一种类型,字面上意思是"具有至少一个带有签名int的方法a的任何对象"

6
我不理解OCaml语法中为什么使用“..”而不是类型变量。在PureScript中,我可以定义“forall r. { label :: String | r } -> { label :: String | r } -> String”。现在两个记录必须具有相同的行类型。但是我也可以用另一个类型变量替换第二个“r”,以表示两个行类型可能不同。在OCaml中如何实现这一点? - user6445533
1
你能否在其他答案提到的“多态变量”中添加一个示例呢? - Will Ness
@WillNess:这个例子在使用多态变体时效果不佳,因为它使用了行积,正如Patj所指出的那样,多态变体用于表示行和 - Richard-Degenne
1
@RichouHunter 我猜ocaml的方法只是稍微不那么严格,但对于大多数情况仍然足够。无论如何,在purescript中,行多态性(用于乘积类型)很常见,因为没有子类型,并且它可以很好地与类型推断/统一配合使用。此外,行多态性不涉及子类型所涉及的逆变/协变/双变量。遗憾的是,它没有被更多的语言支持。 - user6445533
2
我不理解ocaml语法的是为什么要使用..而不是类型变量。因为“..”是一个隐式的类型变量。你可以用“as”来明确它,比如,在OCaml中,你的例子将是:(<标签:字符串;..> as 'a)->'a - ivg
显示剩余5条评论

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