我第一次看到OCaml,有一些F#和Haskell背景。因此,很多东西都很熟悉,但有一点不是“开放”和“封闭”联合的概念(使用反引号和[<语法])。
它们的用途和使用频率是多少?
答案 0 :(得分:18)
gasche's answer has good advice。我将更多地解释开放和封闭的工会。
首先,你需要区分两种联合:基本变体(没有反引号)和多态变体(带反引号)。
M1
和M2
中定义两个具有相同构造函数名称的类型,则您有不同的类型。 M1.Foo
和M2.Foo
是不同的构造函数。无论你在哪里使用它,`Foo
总是相同的构造函数。多态变体类型描述了类型可能具有的构造函数。但是许多多态变体类型并不完全清楚 - 它们包含(行)类型变量。考虑空列表[]
:其类型为'a list
,并且可以在将更多特定类型分配给'a
的许多上下文中使用它。例如:
# let empty_list = [];;
val empty_list : 'a list = []
# let list_of_lists = [] :: empty_list;;
val list_of_lists : 'a list list = [[]]
# let list_of_integers = 3 :: empty_list;;
val list_of_integers : int list = [3]
行类型变量也是如此。一个写入[> … ]
的开放类型有一个行变量,可以在每次使用该值时实例化以覆盖更多构造函数。
# let foo = `Foo;;
val foo : [> `Foo ] = `Foo
# let bar = `Bar;;
val bar : [> `Bar ] = `Bar
# let foobar = [foo; bar];;
val foobar : [> `Bar | `Foo ] list = [`Foo; `Bar]
仅仅因为构造函数出现在类型中并不意味着该类型的每次使用都必须允许所有构造函数。 [> …]
表示类型必须至少包含这些构造函数,并且双重[< …]
表示类型必须至多包含这些构造函数。考虑这个功能:
# let h = function `Foo -> `Bar | `Bar -> `Foo;;
val h : [< `Bar | `Foo ] -> [> `Bar | `Foo ] = <fun>
h
只能处理Foo
和Bar
,因此输入类型可能不允许其他构造函数;但是可以在仅允许h
的类型上调用Foo
。相反,h
可能会返回Foo
或Bar
,而使用h
的任何上下文都必须允许Foo
和Bar
(并且可能允许其他)。
当类型的最小和最大构造函数要求匹配时,会出现封闭类型。例如,让我们添加h
必须具有相同输入和输出类型的约束:
# let hh : 'a -> 'a = function `Foo -> `Bar | `Bar -> `Foo;;
val hh : [ `Bar | `Foo ] -> [ `Bar | `Foo ] = <fun>
封闭类型很少从类型推断中自然产生。大多数时候,就像这里一样,它们是用户注释的结果。当您使用多态注释时,最好定义命名类型并至少在每个顶级函数中使用它们。否则,推断类型可能会比您想象的更加通用。虽然这很少打开通向bug的方法,但这通常意味着任何类型的错误都会在以后诊断出来,并且会生成很长的错误消息,很难找到有用的位。
我建议阅读和处理(即重新键入顶层中的示例,稍微玩一下以确保理解每一步)polymorphic variant tutorial in the Ocaml manual。
答案 1 :(得分:13)
您需要阅读Jacques Garrigue的“使用多态变体进行代码重用”:
http://www.math.nagoya-u.ac.jp/~garrigue/papers/fose2000.html
多态变体的问题在于它们非常灵活,类型推断无法帮助您解决多态变体代码中的错误。例如,如果您输错了一个构造函数名称,则编译器将无法标记错误,它只会使用通常的构造函数和拼写错误的构造函数推断出稍微不同的类型。只有当您尝试将错误代码与对变量进行严格假设的函数(封闭模式匹配)组合在一起时,才会发现错误,并提供笨拙的错误消息。
我对多态变体用户的建议是大量使用注释来控制类型检查:每次将多态变量作为输入或输出一个时,该函数应该使用变量部分的精确类型进行注释。这将使您免受大多数推理问题的影响,并迫使您构建一组富有表现力的类型定义,这些类型定义可以组合并帮助推理您的程序。