From d5750f4f5658618d00f7a8952d9778f4d3c66455 Mon Sep 17 00:00:00 2001 From: Davirain Date: Thu, 4 Jul 2024 15:42:20 +0800 Subject: [PATCH] update --- .../ch3_advanced_patten_matching.md | 54 ++++++------- .../ch3_algebraic_data_types.md | 76 +++++++++---------- .../ch3_association_lists.md | 4 +- .../ch3_example_natural_number.md | 8 +- src/ocaml_programming/ch3_example_tree.md | 13 ++-- src/ocaml_programming/ch3_exceptions.md | 46 +++++------ src/ocaml_programming/ch3_options.md | 30 ++++---- .../ch3_records_and_tuples.md | 46 +++++------ src/ocaml_programming/ch3_type_synonyms.md | 2 +- 9 files changed, 139 insertions(+), 140 deletions(-) diff --git a/src/ocaml_programming/ch3_advanced_patten_matching.md b/src/ocaml_programming/ch3_advanced_patten_matching.md index 349987b..c658895 100644 --- a/src/ocaml_programming/ch3_advanced_patten_matching.md +++ b/src/ocaml_programming/ch3_advanced_patten_matching.md @@ -2,17 +2,17 @@ 以下是一些有用的额外模式形式: -- p1 | ... | pn :一个“或”模式;如果与任何一个单独模式 pi 匹配成功,则匹配成功,这些模式按从左到右的顺序尝试。所有模式必须绑定相同的变量。 -- (p : t) :带有显式类型注释的模式。 -- c :在这里, c 表示任何常量,如整数字面值、字符串字面值和布尔值。 -- 'ch1'..'ch2' :在这里, ch 表示一个字符字面量。例如, 'A'..'Z' 匹配任何大写字母。 -- p when e :仅当 e 评估为 true 时,才匹配 p 。 +- `p1 | ... | pn` :一个“或”模式;如果与任何一个单独模式 `pi` 匹配成功,则匹配成功,这些模式按从左到右的顺序尝试。所有模式必须绑定相同的变量。 +- `(p : t)` :带有显式类型注释的模式。 +- `c` :在这里, `c` 表示任何常量,如整数字面值、字符串字面值和布尔值。 +- `'ch1'..'ch2'` :在这里, `ch` 表示一个字符字面量。例如, `'A'..'Z'` 匹配任何大写字母。 +- `p when e` :仅当 `e` 评估为 `true` 时,才匹配 `p` 。 您可以在[手册](https://ocaml.org/manual/5.2/patterns.html)中阅读有关所有图案形式的信息。 ## 使用 Let 进行模式匹配 -到目前为止,我们一直在使用的 let 表达式的语法实际上是 OCaml 允许的完整语法的一个特例。该语法是: +到目前为止,我们一直在使用的 `let` 表达式的语法实际上是 OCaml 允许的完整语法的一个特例。该语法是: ```ocaml let p = e1 in e2 @@ -20,34 +20,34 @@ let p = e1 in e2 也就是说,绑定的左侧实际上可能是一个模式,而不仅仅是一个标识符。当然,变量标识符在我们的有效模式列表中,这就是为什么我们迄今为止学习的语法只是一个特例。 -鉴于这种语法,我们重新审视 let 表达式的语义。 +鉴于这种语法,我们重新审视 `let` 表达式的语义。 **动态语义学** -评估 let p = e1 in e2 -- 评估 e1 的值为 v1 -- 匹配 v1 与模式 p 。如果不匹配,则引发异常 Match_failure 。否则,如果匹配,则产生一组绑定 +评估 `let p = e1 in e2` +- 评估 `e1` 的值为 `v1` +- 匹配 `v1` 与模式 `p` 。如果不匹配,则引发异常 `Match_failure` 。否则,如果匹配,则产生一组绑定 。 -- 将 e2 中的绑定 b 替换为新表达式 e2' 。 -- 评估 e2' 的值为 v2 -- 评估 let 表达式的结果是 v2 。 +- 将 `e2` 中的绑定 `b` 替换为新表达式 `e2'` 。 +- 评估 `e2'` 的值为 `v2` +- 评估 `let` 表达式的结果是 `v2` 。 **静态语义** -- 如果所有以下条件成立,则 (let p = e1 in e2) : t2 : - - e1 : t1 - - p 中的模式变量为 x1..xn - - 在假设对于 1..n 中的所有 i 都成立 xi : ti 的情况下, e2 : t2 +- 如果所有以下条件成立,则 `(let p = e1 in e2) : t2` : + - `e1 : t1` + - `p` 中的模式变量为 `x1..xn` + - 在假设对于 `1..n` 中的所有 `i` 都成立 `xi : ti` 的情况下, `e2 : t2` **让我们来看一下定义** -与以往一样,let 定义可以被理解为一个 let 表达式,其主体尚未给出。因此,它们的语法可以泛化为 +与以往一样,`let` 定义可以被理解为一个 `let` 表达式,其主体尚未给出。因此,它们的语法可以泛化为 ```ocaml let p = e ``` -它们的语义与之前的 let 表达式的语义一致。 +它们的语义与之前的 `let` 表达式的语义一致。 ## 使用函数进行模式匹配 @@ -59,20 +59,20 @@ let f p1 ... pn = e (* function definition at toplevel *) fun p1 ... pn -> e (* anonymous function *) ``` -我们需要关心的真正原始的句法形式是 fun p -> e 。让我们重新审视匿名函数的语义及其在该形式下的应用;其他形式的变化源自以下内容: +我们需要关心的真正原始的句法形式是 `fun p -> e` 。让我们重新审视匿名函数的语义及其在该形式下的应用;其他形式的变化源自以下内容: **静态语义** -- 设 x1..xn 为出现在 p 中的模式变量。如果假设 x1 : t1 和 x2 : t2 和…和 xn : tn ,我们可以得出 p : t 和 e :u 的结论,则 fun p -> e : t -> u 。 +- 设 `x1..xn` 为出现在 `p` 中的模式变量。如果假设 `x1 : t1` 和 `x2 : t2` 和…和 `xn : tn` ,我们可以得出 `p : t` 和 `e :u` 的结论,则 `fun p -> e : t -> u` 。 - 应用程序的类型检查规则保持不变。 **动态语义** - 匿名函数的评估规则保持不变。 -- 评估 e0 e1 - - 将 e0 评估为匿名函数 fun p -> e ,并将 e1 评估为值 v1 - - 匹配 v1 与模式 p 。如果不匹配,则引发异常 Match_failure 。否则,如果匹配,则产生一组绑定 b。 - - 将 e 中的绑定 b 替换为新表达式 e' 。 - - 评估 e' 的值为 v ,这是评估 e0 e1 的结果。 +- 评估 `e0 e1` + - 将 `e0` 评估为匿名函数 `fun p -> e` ,并将 `e1` 评估为值 `v1` + - 匹配 `v1` 与模式 `p` 。如果不匹配,则引发异常 `Match_failure` 。否则,如果匹配,则产生一组绑定 `b`。 + - 将 `e` 中的绑定 `b` 替换为新表达式 `e'` 。 + - 评估 `e'` 的值为 `v` ,这是评估 `e0 e1` 的结果。 ## 模式匹配示例 @@ -147,7 +147,7 @@ val fst : 'a * 'b -> 'a = val snd : 'a * 'b -> 'b = ``` -fst 和 snd 实际上已经在标准库中为您定义好了。 +`fst` 和 `snd` 实际上已经在标准库中为您定义好了。 最后,以下是获取三元组的第三个组件的几种方法: diff --git a/src/ocaml_programming/ch3_algebraic_data_types.md b/src/ocaml_programming/ch3_algebraic_data_types.md index efd5f28..b163747 100644 --- a/src/ocaml_programming/ch3_algebraic_data_types.md +++ b/src/ocaml_programming/ch3_algebraic_data_types.md @@ -16,7 +16,7 @@ type peff = ENormal | ENotVery | Esuper -作为一个运行示例,这里有一个变体类型 shape ,它不仅仅是枚举值: +作为一个运行示例,这里有一个变体类型 `shape` ,它不仅仅是枚举值: ```ocaml type point = float * float @@ -34,13 +34,13 @@ type point = float * float type shape = Point of point | Circle of point * float | Rect of point * point ``` -这种类型, shape ,代表着一个点、一个圆或一个矩形的形状。一个点由一个构造函数 Point 表示,它携带一些额外的数据,即类型为 point 的值。一个圆由一个构造函数 Circle 表示,它携带两个数据片段:一个是类型为 point 的,另一个是类型为 float 的。这些数据代表圆的中心和半径。一个矩形由一个构造函数 Rect 表示,它携带另外两个点。 +这种类型, `shape` ,代表着一个点、一个圆或一个矩形的形状。一个点由一个构造函数 `Point` 表示,它携带一些额外的数据,即类型为 `point` 的值。一个圆由一个构造函数 `Circle` 表示,它携带两个数据片段:一个是类型为 `point` 的,另一个是类型为 `float` 的。这些数据代表圆的中心和半径。一个矩形由一个构造函数 `Rect` 表示,它携带另外两个点。 -这里有几个使用 shape 类型的函数: +这里有几个使用 `shape` 类型的函数: ```ocaml let area = function @@ -66,18 +66,18 @@ val center : shape -> point = ``` -shape 变体类型与我们之前见过的类型相同,因为它是根据一组构造函数定义的。与以往不同的是,这些构造函数携带额外的数据。类型 shape 的每个值都是从这些构造函数中的一个精确形成的。有时我们称构造函数为标签,因为它标记携带的数据来自特定的构造函数。 +`shape` 变体类型与我们之前见过的类型相同,因为它是根据一组构造函数定义的。与以往不同的是,这些构造函数携带额外的数据。类型 `shape` 的每个值都是从这些构造函数中的一个精确形成的。有时我们称构造函数为标签,因为它标记携带的数据来自特定的构造函数。 -变体类型有时被称为标记联合。该类型的每个值都来自构造函数携带的基础类型的所有值的并集。例如,对于 shape 类型,每个值都标记为 Point 或 Circle 或 Rect ,并携带一个值。 +变体类型有时被称为标记联合。该类型的每个值都来自构造函数携带的基础类型的所有值的并集。例如,对于 `shape` 类型,每个值都标记为 `Point` 或 `Circle` 或 `Rect` ,并携带一个值。 -- 所有 point 值的集合,与并集 -- 所有 point * float 值的集合,与并集 -- 所有 point * point 值的集合。 +- 所有 `point` 值的集合,与并集 +- 所有 `point * float` 值的集合,与并集 +- 所有 `point * point` 值的集合。 这些变体类型的另一个名称是代数数据类型。这里的“代数”指的是变体类型包含前一讲中定义的和类型和积类型。和类型源自于一个变体的值是由其中一个构造器形成的事实。积类型源自于构造器可以携带元组或记录的事实,这些元组或记录的值分别来自于它们各自的组件类型。 -使用变体,我们可以以一种类型安全的方式表示代表多个其他类型的联合类型。例如,这里是一个代表 string 或 int 的类型: +使用变体,我们可以以一种类型安全的方式表示代表多个其他类型的联合类型。例如,这里是一个代表 `string` 或 `int` 的类型: ```ocaml type string_or_int = @@ -150,7 +150,7 @@ val double_right : t -> int = type t = C1 [of t1] | ... | Cn [of tn] ``` -上面的方括号表示 of ti 是可选的。每个构造函数可以单独不携带数据或携带数据。我们称不携带数据的构造函数为常量;携带数据的构造函数称为非常量。 +上面的方括号表示 `of ti` 是可选的。每个构造函数可以单独不携带数据或携带数据。我们称不携带数据的构造函数为常量;携带数据的构造函数称为非常量。 编写一个变体表达式: @@ -164,12 +164,12 @@ C e C ``` -取决于构造函数名称 C 是非常量还是常量。 +取决于构造函数名称 `C` 是非常量还是常量。 **动态语义学** -- 如果 e ==> v ,那么 C e ==> C v ,假设 C 是非常数的。 -- C 已经是一个数值,假设 C 是常数。 +- 如果 `e ==> v` ,那么 `C e ==> C v` ,假设 `C` 是非常数的。 +- `C` 已经是一个数值,假设 `C` 是常数。 **静态语义学** @@ -180,11 +180,11 @@ C 我们将以下新的模式形式添加到合法模式列表中: -- C p +- `C p` 我们将模式与值匹配并生成绑定的定义扩展如下: -- 如果 p 匹配 v 并产生绑定 b ,那么 C p 匹配 C v 并产生绑定 b。 +- 如果 `p` 匹配 `v` 并产生绑定 `b` ,那么 `C p` 匹配 `C v` 并产生绑定 `b`。 ## 全部情况 @@ -228,7 +228,7 @@ type color = Blue | Red | Green val string_of_color : color -> string = ``` -但是由于中间有上千行代码,你忘记了 string_of_color 需要更新。现在,突然间,你变成了红绿色盲: +但是由于中间有上千行代码,你忘记了 `string_of_color` 需要更新。现在,突然间,你变成了红绿色盲: ```ocaml string_of_color Green @@ -238,7 +238,7 @@ string_of_color Green - : string = "red" ``` -问题在于模式匹配中的通配情况 string_of_color :使用通配符模式匹配任何内容的最终情况。这样的代码对于将来对变体类型的更改不够健壮。 +问题在于模式匹配中的通配情况 `string_of_color` :使用通配符模式匹配任何内容的最终情况。这样的代码对于将来对变体类型的更改不够健壮。 如果您最初将函数编码为以下内容,生活会更美好: @@ -259,7 +259,7 @@ Green val string_of_color : color -> string = ``` -OCaml 类型检查器现在提醒您,您尚未更新 string_of_color 以考虑新构造函数。 +OCaml 类型检查器现在提醒您,您尚未更新 `string_of_color` 以考虑新构造函数。 故事的道德是:捕捉所有情况会导致错误的代码。避免使用它们。 @@ -267,7 +267,7 @@ OCaml 类型检查器现在提醒您,您尚未更新 string_of_color 以考虑 -变体类型可能在其自身的内部提及自己的名称。例如,这里是一个变体类型,可以用来表示类似于 int list 的东西: +变体类型可能在其自身的内部提及自己的名称。例如,这里是一个变体类型,可以用来表示类似于 `int list` 的东西: ```ocaml type intlist = Nil | Cons of int * intlist @@ -298,9 +298,9 @@ val length : intlist -> int = val empty : intlist -> bool = ``` -请注意,在 intlist 的定义中,我们将 Cons 构造函数定义为携带包含 intlist 的值。这使得类型 intlist 成为递归的:它是根据自身定义的。 +请注意,在 `intlist` 的定义中,我们将 `Cons` 构造函数定义为携带包含 `intlist` 的值。这使得类型 `intlist` 成为递归的:它是根据自身定义的。 -如果使用 and 关键字,类型可能会相互递归 +如果使用 `and` 关键字,类型可能会相互递归 ```ocaml type node = {value : int; next : mylist} @@ -364,11 +364,11 @@ File "[15]", line 1, characters 0-14: Error: The type abbreviation t is cyclic ``` -尽管 node 是一种合法的类型定义,但由于涉及循环性,无法构造该类型的值:要构造出存在的第一个 node 值,您已经需要一个类型为 node 的值存在。稍后,当我们涵盖命令式特性时,我们将看到类似的想法用于可变链表(但成功地)。 +尽管 `node` 是一种合法的类型定义,但由于涉及循环性,无法构造该类型的值:要构造出存在的第一个 `node` 值,您已经需要一个类型为 `node` 的值存在。稍后,当我们涵盖命令式特性时,我们将看到类似的想法用于可变链表(但成功地)。 ## 参数化变体 -变体类型可以基于其他类型进行参数化。例如,上面的 intlist 类型可以泛化为提供列表(我们自己编码)的任何类型: +变体类型可以基于其他类型进行参数化。例如,上面的 `intlist` 类型可以泛化为提供列表(我们自己编码)的任何类型: ```ocaml type 'a mylist = Nil | Cons of 'a * 'a mylist @@ -389,9 +389,9 @@ val lst3 : int mylist = Cons (3, Nil) val lst_hi : string mylist = Cons ("hi", Nil) ``` -在这里, mylist 是一种类型构造器而不是一种类型:没有办法编写类型为 mylist 的值。但是我们可以编写类型为 int mylist 的值(例如, lst3 )和 string mylist 的值(例如, lst_hi )。将类型构造器视为类似于函数,但是它将类型映射到类型,而不是将值映射到值。 +在这里, `mylist` 是一种类型构造器而不是一种类型:没有办法编写类型为 `mylist` 的值。但是我们可以编写类型为 `int mylist` 的值(例如, `lst3` )和 `string mylist` 的值(例如, `lst_hi` )。将类型构造器视为类似于函数,但是它将类型映射到类型,而不是将值映射到值。 -这里是一些关于 'a mylist 的函数: +这里是一些关于 `'a mylist` 的函数: ```ocaml let rec length : 'a mylist -> int = function @@ -411,7 +411,7 @@ val length : 'a mylist -> int = val empty : 'a mylist -> bool = ``` -请注意,每个函数的主体与其之前的定义 intlist 没有变化。我们改变的只是类型注释。甚至可以安全地省略这一点。 +请注意,每个函数的主体与其之前的定义 `intlist` 没有变化。我们改变的只是类型注释。甚至可以安全地省略这一点。 ```ocaml let rec length = function @@ -431,9 +431,9 @@ val length : 'a mylist -> int = val empty : 'a mylist -> bool = ``` -我们刚刚编写的函数是一种称为参数多态性的语言特性的示例。这些函数不关心 'a 在 'a mylist 中是什么,因此它们可以很好地在 int mylist 或 string mylist 或任何其他 (whatever) mylist 上工作。单词“多态性”基于希腊词根“poly”(许多)和“morph”(形式)。类型为 'a mylist 的值可能有许多形式,取决于实际类型 'a 。 +我们刚刚编写的函数是一种称为参数多态性的语言特性的示例。这些函数不关心 `'a` 在 `'a mylist` 中是什么,因此它们可以很好地在 `int mylist` 或 `string mylist` 或任何其他 (whatever) mylist 上工作。单词“多态性”基于希腊词根“`poly`”(许多)和“`morph`”(形式)。类型为 `'a mylist` 的值可能有许多形式,取决于实际类型 `'a` 。 -一旦您对类型 'a 可能是什么加以限制,您就放弃了一些多态性。例如, +一旦您对类型 `'a` 可能是什么加以限制,您就放弃了一些多态性。例如, ```ocaml let rec sum = function @@ -441,7 +441,7 @@ let rec sum = function | Cons (h, t) -> h + sum t ``` -我们使用 ( + ) 运算符与列表的头部结合的事实限制了头部元素必须是 int ,因此所有元素必须是 int 。这意味着 sum 必须接受 int mylist ,而不是其他类型的 'a mylist 。 +我们使用 `( + )` 运算符与列表的头部结合的事实限制了头部元素必须是 `int` ,因此所有元素必须是 `int` 。这意味着 `sum` 必须接受 `int mylist` ,而不是其他类型的 `'a mylist` 。 还可以为参数化类型设置多个类型参数,这种情况下需要使用括号: @@ -461,7 +461,7 @@ val x : (int, string) pair = {first = 2; second = "hello"} ## 多态变体 -到目前为止,每当您想要定义一个变体类型时,您必须给它一个名称,比如 day , shape 或 'a mylist : +到目前为止,每当您想要定义一个变体类型时,您必须给它一个名称,比如 `day` , `shape` 或 `'a mylist` : ```ocaml type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat @@ -486,7 +486,7 @@ type shape = Point of point | Circle of point * float | Rect of point * point type 'a mylist = Nil | Cons of 'a * 'a mylist ``` -偶尔,您可能仅需要一种变体类型来表示单个函数的返回值。例如,这里有一个函数 f ,可以返回 int 或 ∞ +偶尔,您可能仅需要一种变体类型来表示单个函数的返回值。例如,这里有一个函数 `f`,可以返回 `int` 或 `∞` ;您需要定义一个变体类型来表示该结果: ```ocaml @@ -506,7 +506,7 @@ type fin_or_inf = Finite of int | Infinity val f : int -> fin_or_inf = ``` -这个定义的缺点是,即使在程序的大部分部分中都不会使用 fin_or_inf ,你还是被迫定义它。 +这个定义的缺点是,即使在程序的大部分部分中都不会使用 `fin_or_inf` ,你还是被迫定义它。 OCaml 中还有另一种支持这种编程的变体:多态变体。多态变体就像变体一样,只是: @@ -514,7 +514,7 @@ OCaml 中还有另一种支持这种编程的变体:多态变体。多态变 - 多态变体类型没有名称。(因此,这一特性的另一个名称可以是“匿名变体”。) - 多态变体的构造者以反引号字符开头。 -使用多态变体,我们可以重写 f : +使用多态变体,我们可以重写 `f` : ```ocaml let f = function @@ -527,7 +527,7 @@ let f = function val f : int -> [> `Finite of int | `Infinity ] = ``` -这种类型表示 f 要么对某些 n : int 返回 `Finite n ,要么 `Infinity 。方括号不表示列表,而是一组可能的构造函数。 > 符号意味着针对该类型的值进行模式匹配的任何代码必须至少处理构造函数 `Finite 和 `Infinity ,可能还有更多。例如,我们可以这样写: +这种类型表示 `f` 要么对某些 `n : int` 返回 ``Finite n` ,要么 ``Infinity`` 。方括号不表示列表,而是一组可能的构造函数。 `>` 符号意味着针对该类型的值进行模式匹配的任何代码必须至少处理构造函数 ``Finite` 和 ``Infinity` ,可能还有更多。例如,我们可以这样写: ```ocaml match f 3 with @@ -542,7 +542,7 @@ match f 3 with - : string = "finite" ``` -模式匹配包括除 `Finite 或 `Infinity 之外的构造函数是完全可以的,因为 f 保证不会返回除此之外的任何构造函数。 +模式匹配包括除 `Finite 或 `Infinity 之外的构造函数是完全可以的,因为 `f` 保证不会返回除此之外的任何构造函数。 在课程的后期我们会看到多态变体还有其他更具说服力的用途。它们在库中特别有用。目前,我们通常会建议您遏制对多态变体的广泛使用,因为它们的类型可能变得难以管理。 @@ -554,7 +554,7 @@ OCaml 的内置列表数据类型实际上是一个递归的、参数化的变 type 'a list = [] | ( :: ) of 'a * 'a list ``` -所以 list 实际上只是一个类型构造器,具有值构造器 [] (我们发音为“nil”)和 :: (我们发音为“cons”)。 +所以 `list` 实际上只是一个类型构造器,具有值构造器 `[]` (我们发音为“`nil`”)和 `::` (我们发音为“cons”)。 OCaml 的内置选项数据类型实际上也是一个参数化的变体。它的定义如下: @@ -562,6 +562,6 @@ OCaml 的内置选项数据类型实际上也是一个参数化的变体。它 type 'a option = None | Some of 'a ``` -所以 option 实际上只是一个类型构造器,带有值构造器 None 和 Some 。 +所以 `option` 实际上只是一个类型构造器,带有值构造器 `None` 和 `Some` 。 -您可以在核心 OCaml 库中看到 list 和 option 都有[定义](https://ocaml.org/manual/5.2/core.html)。 +您可以在核心 OCaml 库中看到 `list` 和 `option` 都有[定义](https://ocaml.org/manual/5.2/core.html)。 diff --git a/src/ocaml_programming/ch3_association_lists.md b/src/ocaml_programming/ch3_association_lists.md index 9c7abb5..ee1c88d 100644 --- a/src/ocaml_programming/ch3_association_lists.md +++ b/src/ocaml_programming/ch3_association_lists.md @@ -35,6 +35,6 @@ val insert : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) list = val lookup : 'a -> ('a * 'b) list -> 'b option = ``` -insert 函数只是简单地在列表的前面添加一个从键到值的新映射。它不会去检查键是否已经在列表中。 lookup 函数从左到右遍历列表。因此,如果列表中恰好有多个给定键的映射,只会返回最近插入的那个。 +`insert` 函数只是简单地在列表的前面添加一个从键到值的新映射。它不会去检查键是否已经在列表中。 `lookup` 函数从左到右遍历列表。因此,如果列表中恰好有多个给定键的映射,只会返回最近插入的那个。 -插入在关联列表中是常数时间,查找是线性时间。虽然字典有更高效的实现方式,我们将在课程后面学习一些,但关联列表是一个非常简单和有用的实现方式,适用于不需要高性能的小型字典。OCaml 标准库在 List 模块中有关联列表的函数;在文档中查找 List.assoc 和其下面的函数。我们刚刚写的 lookup 实际上已经被定义为 List.assoc_opt 。标准库中没有预定义的 insert 函数,因为在上面简单地将一对元素连接起来是如此微不足道。 +插入在关联列表中是常数时间,查找是线性时间。虽然字典有更高效的实现方式,我们将在课程后面学习一些,但关联列表是一个非常简单和有用的实现方式,适用于不需要高性能的小型字典。OCaml 标准库在 `List` 模块中有关联列表的函数;在文档中查找 `List.assoc` 和其下面的函数。我们刚刚写的 `lookup` 实际上已经被定义为 `List.assoc_opt` 。标准库中没有预定义的 `insert` 函数,因为在上面简单地将一对元素连接起来是如此微不足道。 diff --git a/src/ocaml_programming/ch3_example_natural_number.md b/src/ocaml_programming/ch3_example_natural_number.md index efb97cd..d040dc0 100644 --- a/src/ocaml_programming/ch3_example_natural_number.md +++ b/src/ocaml_programming/ch3_example_natural_number.md @@ -2,7 +2,7 @@ 我们可以定义一个递归变体,它的行为类似于数字,从而证明我们实际上并不一定需要将数字内置到 OCaml 中!(不过出于效率考虑,将数字内置进去是件好事。) -自然数要么是零,要么是另一个自然数的后继。这是你在数理逻辑课程中可能会定义自然数的方式,它自然地导致以下 OCaml 类型 nat : +自然数要么是零,要么是另一个自然数的后继。这是你在数理逻辑课程中可能会定义自然数的方式,它自然地导致以下 OCaml 类型 `nat` : ```ocaml type nat = Zero | Succ of nat @@ -12,7 +12,7 @@ type nat = Zero | Succ of nat type nat = Zero | Succ of nat ``` -我们已经定义了一个新类型 nat , Zero 和 Succ 是这种类型值的构造函数。这使我们能够构建具有任意数量嵌套 Succ 构造函数的表达式。这样的值就像自然数: +我们已经定义了一个新类型 `nat` , `Zero` 和 `Succ` 是这种类型值的构造函数。这使我们能够构建具有任意数量嵌套 `Succ` 构造函数的表达式。这样的值就像自然数: ```ocaml let zero = Zero @@ -42,7 +42,7 @@ val three : nat = Succ (Succ (Succ Zero)) val four : nat = Succ (Succ (Succ (Succ Zero))) ``` -现在我们可以编写函数来操作这种类型的值。在下面的代码中,我们将写很多类型注释,以帮助读者跟踪哪些值是 nat 而不是 int ;当然,编译器不需要我们的帮助。 +现在我们可以编写函数来操作这种类型的值。在下面的代码中,我们将写很多类型注释,以帮助读者跟踪哪些值是 `nat` 而不是 `int` ;当然,编译器不需要我们的帮助。 ```ocaml let iszero = function @@ -75,7 +75,7 @@ let rec add n1 n2 = val add : nat -> nat -> nat = ``` -我们可以将 nat 值转换为类型 int ,反之亦然: +我们可以将 `nat` 值转换为类型 `int` ,反之亦然: ```ocaml let rec int_of_nat = function diff --git a/src/ocaml_programming/ch3_example_tree.md b/src/ocaml_programming/ch3_example_tree.md index 63d5d1d..9c63ab8 100644 --- a/src/ocaml_programming/ch3_example_tree.md +++ b/src/ocaml_programming/ch3_example_tree.md @@ -18,7 +18,7 @@ type 'a tree = type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree ``` -一个节点携带类型为 'a 的数据项,并具有左子树和右子树。叶子为空。将此定义与列表的定义进行比较,注意它们结构是多么相似: +一个节点携带类型为 `'a` 的数据项,并具有左子树和右子树。叶子为空。将此定义与列表的定义进行比较,注意它们结构是多么相似: ```ocaml type 'a tree = type 'a mylist = @@ -26,7 +26,7 @@ type 'a tree = type 'a mylist = | Node of 'a * 'a tree * 'a tree | Cons of 'a * 'a mylist ``` -唯一的基本区别在于 Cons 包含一个子列表,而 Node 包含两个子树。 +唯一的基本区别在于 `Cons` 包含一个子列表,而 `Node` 包含两个子树。 这里是构建一个小树的代码: @@ -57,7 +57,7 @@ val t : int tree = Node (5, Node (6, Leaf, Leaf), Node (7, Leaf, Leaf))) ``` -树的大小是指其中节点的数量(即 Node 个,而不是 Leaf 个)。例如,上面树 t 的大小为 7。这里是一个函数 size : 'a tree -> int ,用于返回树中节点的数量: +树的大小是指其中节点的数量(即 `Node` 个,而不是 Leaf 个)。例如,上面树 `t` 的大小为 7。这里是一个函数 `size : 'a tree -> int` ,用于返回树中节点的数量: ```ocaml let rec size = function @@ -121,7 +121,7 @@ let rec mem x = function val mem : 'a -> 'a tree -> bool = ``` -函数名称 mem 是“成员”的缩写;标准库通常使用这个名称的函数来实现对集合数据结构的搜索,以确定某个元素是否是该集合的成员。 +函数名称 `mem` 是“成员”的缩写;标准库通常使用这个名称的函数来实现对集合数据结构的搜索,以确定某个元素是否是该集合的成员。 这是一个计算树的先序遍历的函数,其中每个节点在其任何子节点之前被访问,通过构建一个列表,其中值按照它们被访问的顺序出现: @@ -129,7 +129,6 @@ val mem : 'a -> 'a tree -> bool = let rec preorder = function | Leaf -> [] | Node {value; left; right} -> [value] @ preorder left @ preorder right - ``` ```ocaml @@ -144,7 +143,7 @@ preorder t - : int list = [2; 1; 3] ``` -尽管上面的代码中算法非常清晰,但由于 @ 运算符,在不平衡树上它需要二次时间。这个问题可以通过引入一个额外的参数 acc 来解决,在每个节点累积值,尽管这会使代码变得不太清晰。 +尽管上面的代码中算法非常清晰,但由于 `@` 运算符,在不平衡树上它需要二次时间。这个问题可以通过引入一个额外的参数 `acc` 来解决,在每个节点累积值,尽管这会使代码变得不太清晰。 ```ocaml let preorder_lin t = @@ -158,4 +157,4 @@ let preorder_lin t = val preorder_lin : 'a tree -> 'a list = ``` -上述版本在树中每个 Node 使用了恰好一个 :: 操作,使其具有线性时间。 +上述版本在树中每个 `Node` 使用了恰好一个 `::` 操作,使其具有线性时间。 diff --git a/src/ocaml_programming/ch3_exceptions.md b/src/ocaml_programming/ch3_exceptions.md index d8b7912..7006e22 100644 --- a/src/ocaml_programming/ch3_exceptions.md +++ b/src/ocaml_programming/ch3_exceptions.md @@ -9,7 +9,7 @@ OCaml 具有类似于许多其他编程语言的异常机制。使用以下语 exception E of t ``` -其中 E 是构造函数名称, t 是类型。 of t 是可选的。注意这与定义变体类型的构造函数类似。例如: +其中 `E` 是构造函数名称, `t` 是类型。 `of t` 是可选的。注意这与定义变体类型的构造函数类似。例如: ```ocaml exception A @@ -25,7 +25,7 @@ exception Code of int exception Details of string ``` -要创建一个异常值,使用创建变体值时相同的语法。例如,这里是一个异常值,其构造函数是 Failure ,携带了一个 string : +要创建一个异常值,使用创建变体值时相同的语法。例如,这里是一个异常值,其构造函数是 `Failure` ,携带了一个 `string` : ```ocaml Failure "something went wrong" @@ -37,13 +37,13 @@ Failure "something went wrong" 这个构造函数是在标准库中[预定义](https://ocaml.org/manual/5.2/core.html#ss:predef-exn)的,是 OCaml 程序员经常使用的更常见的异常之一。 -要引发异常值 e ,只需简单地编写 +要引发异常值 `e` ,只需简单地编写 ```ocaml raise e ``` -标准库中有一个方便的函数 failwith : string -> 'a ,它引发 Failure 。也就是说, failwith s 等同于 raise (Failure s) 。 +标准库中有一个方便的函数 `failwith : string -> 'a` ,它引发 `Failure` 。也就是说, `failwith s` 等同于 `raise (Failure s)` 。 @@ -56,11 +56,11 @@ try e with | pn -> en ``` -表达式 e 可能会引发异常。如果没有引发异常,整个 try 表达式将评估为 e 的值。如果 e 引发异常值 v ,该值 v 将与提供的模式进行匹配,就像 match 表达式一样。 +表达式 `e` 可能会引发异常。如果没有引发异常,整个 `try` 表达式将评估为 `e` 的值。如果 `e` 引发异常值 `v` ,该值 `v` 将与提供的模式进行匹配,就像 `match` 表达式一样。 ## 异常情况是可扩展的变体 -所有异常值都具有类型 exn ,这是核心中定义的一种变体。不过,它是一种不寻常的变体,称为可扩展变体,允许在定义变体类型本身之后定义新的变体构造函数。如果您感兴趣,可以查看 OCaml 手册了解更多关于可扩展变体的信息。 +所有异常值都具有类型 `exn` ,这是核心中定义的一种变体。不过,它是一种不寻常的变体,称为可扩展变体,允许在定义变体类型本身之后定义新的变体构造函数。如果您感兴趣,可以查看 OCaml 手册了解更多关于可扩展变体的信息。 ## 异常语义 @@ -72,11 +72,11 @@ try e with - 引发异常 - 或者无法终止(即“无限循环”)。 -到目前为止,我们只介绍了处理这三种情况中的第一种情况的动态语义部分。当我们添加异常时会发生什么?现在,表达式的评估要么产生一个值,要么产生一个异常数据包。数据包不是正常的 OCaml 值;只有语言中的 raise 和 try 才能识别它们。由(例如) Failure "oops" 产生的异常值是由 raise (Failure "oops") 产生的异常数据包的一部分,但数据包不仅包含异常值;例如,还可以有一个堆栈跟踪。 +到目前为止,我们只介绍了处理这三种情况中的第一种情况的动态语义部分。当我们添加异常时会发生什么?现在,表达式的评估要么产生一个值,要么产生一个异常数据包。数据包不是正常的 OCaml 值;只有语言中的 `raise` 和 `try` 才能识别它们。由(例如) `Failure "oops"` 产生的异常值是由 `raise` (Failure "oops") 产生的异常数据包的一部分,但数据包不仅包含异常值;例如,还可以有一个堆栈跟踪。 -对于除 try 之外的任何表达式 e ,如果 e 的子表达式的评估产生异常数据包 P ,那么 e 的评估会产生数据包 P 。 +对于除 `try` 之外的任何表达式 `e` ,如果 `e` 的子表达式的评估产生异常数据包 `P` ,那么 `e` 的评估会产生数据包 `P` 。 -但现在我们第一次遇到一个问题:子表达式的求值顺序是什么?有时,这个问题的答案可以通过我们已经开发的语义来提供。例如,对于 let 表达式,我们知道绑定表达式必须在主体表达式之前求值。因此,以下代码会引发 A : +但现在我们第一次遇到一个问题:子表达式的求值顺序是什么?有时,这个问题的答案可以通过我们已经开发的语义来提供。例如,对于 `let` 表达式,我们知道绑定表达式必须在主体表达式之前求值。因此,以下代码会引发 `A` : ```ocaml let _ = raise A in raise B;; @@ -89,7 +89,7 @@ Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52 Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150 ``` -在 OCaml 中,对于函数,它并没有正式规定函数和参数的求值顺序,但当前的实现是在函数之前对参数进行求值。因此,以下代码也会引发 A ,除了产生一些编译器警告,即第一个表达式实际上永远不会被应用为函数的参数: +在 OCaml 中,对于函数,它并没有正式规定函数和参数的求值顺序,但当前的实现是在函数之前对参数进行求值。因此,以下代码也会引发 `A` ,除了产生一些编译器警告,即第一个表达式实际上永远不会被应用为函数的参数: ```ocaml (raise B) (raise A) @@ -113,7 +113,7 @@ Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52 Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150 ``` -这两段代码引发相同异常是合理的,因为我们知道 let x = e1 in e2 是 (fun x -> e2) e1 的语法糖。 +这两段代码引发相同异常是合理的,因为我们知道 `let x = e1 in e2` 是 `(fun x -> e2) e1` 的语法糖。 但是以下代码会引发什么异常? @@ -128,7 +128,7 @@ Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52 Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150 ``` -答案是微妙的。语言规范并未规定对成对组件的评估顺序。我们的语义也没有确切确定顺序。(尽管如果你认为是从左到右也是可以原谅的。)因此,程序员实际上不能依赖于该顺序。事实证明,OCaml 的当前实现是从右向左评估的。因此,上面的代码实际上会引发 B 。如果你真的想要强制评估顺序,你需要使用 let 表达式: +答案是微妙的。语言规范并未规定对成对组件的评估顺序。我们的语义也没有确切确定顺序。(尽管如果你认为是从左到右也是可以原谅的。)因此,程序员实际上不能依赖于该顺序。事实证明,OCaml 的当前实现是从右向左评估的。因此,上面的代码实际上会引发 `B` 。如果你真的想要强制评估顺序,你需要使用 `let` 表达式: ```ocaml let a = raise A in @@ -143,9 +143,9 @@ Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52 Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150 ``` -该代码保证会引发 A 而不是 B 。 +该代码保证会引发 `A` 而不是 `B` 。 -一个有趣的边界情况是当一个 raise 表达式本身具有一个引发异常的子表达式时会发生什么: +一个有趣的边界情况是当一个 `raise` 表达式本身具有一个引发异常的子表达式时会发生什么: ```ocaml exception C of string;; @@ -168,9 +168,9 @@ Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52 Called from Topeval.load_lambda in file "toplevel/byte/topeval.ml", line 89, characters 4-150 ``` -该代码最终会引发 D ,因为首先必须评估 C (raise (D "oops")) 的值。这需要评估 raise (D "oops") 的值。这会导致生成一个包含 D "oops" 的数据包,然后该数据包传播并成为评估 C (raise (D "oops")) 的结果,因此也是评估 raise (C (raise (D "oops"))) 的结果。 +该代码最终会引发 `D` ,因为首先必须评估 `C (raise (D "oops"))` 的值。这需要评估 `raise (D "oops")` 的值。这会导致生成一个包含 `D "oops"` 的数据包,然后该数据包传播并成为评估 `C (raise (D "oops"))` 的结果,因此也是评估 `raise (C (raise (D "oops")))` 的结果。 -一旦表达式的评估产生异常数据包 P ,该数据包会传播直至到达一个 try 表达式: +一旦表达式的评估产生异常数据包 `P` ,该数据包会传播直至到达一个 `try` 表达式: ```ocaml try e with @@ -179,7 +179,7 @@ try e with | pn -> en ``` -在 P 内部的异常值与提供的模式进行匹配,使用通常的模式匹配评估规则——有一个例外(再次,双关语)。如果没有任何模式匹配成功,那么不会在新的异常数据包内生成 Match_failure ,而是原始异常数据包 P 会继续传播,直到达到下一个 try 表达式。 +在 `P` 内部的异常值与提供的模式进行匹配,使用通常的模式匹配评估规则——有一个例外(再次,双关语)。如果没有任何模式匹配成功,那么不会在新的异常数据包内生成 `Match_failure` ,而是原始异常数据包 `P` 会继续传播,直到达到下一个 `try` 表达式。 ## 模式匹配 @@ -192,7 +192,7 @@ match List.hd [] with | exception (Failure s) -> s ``` -请注意,上面的代码只是一个标准的 match 表达式,而不是一个 try 表达式。它将 List.hd [] 的值与提供的三个模式进行匹配。正如我们所知, List.hd [] 将引发一个包含值 Failure "hd" 的异常。异常模式 exception (Failure s) 与该值匹配。因此,上述代码将求值为 "hd" 。 +请注意,上面的代码只是一个标准的 `match` 表达式,而不是一个 `try` 表达式。它将 `List.hd []` 的值与提供的三个模式进行匹配。正如我们所知, `List.hd []` 将引发一个包含值 `Failure "hd"` 的异常。异常模式 `exception (Failure s)` 与该值匹配。因此,上述代码将求值为 `"hd"` 。 异常模式是一种语法糖。例如,考虑以下代码: @@ -216,11 +216,11 @@ with | p4 -> e4 ``` -通常情况下,如果存在异常模式和非异常模式,则评估过程如下:尝试评估 e 。如果它产生异常数据包,则使用原始匹配表达式中的异常模式来处理该数据包。如果它不产生异常数据包,而是产生非异常值,则使用原始匹配表达式中的非异常模式来匹配该值。 +通常情况下,如果存在异常模式和非异常模式,则评估过程如下:尝试评估 `e` 。如果它产生异常数据包,则使用原始匹配表达式中的异常模式来处理该数据包。如果它不产生异常数据包,而是产生非异常值,则使用原始匹配表达式中的非异常模式来匹配该值。 ## 异常和 OUnit -如果函数的规范要求它引发异常,您可能希望编写 OUnit 测试来检查函数是否正确执行此操作。以下是如何做到这一点: +如果函数的规范要求它引发异常,您可能希望编写 `OUnit 测试来检查函数是否正确执行此操作。以下是如何做到这一点: ```ocaml open OUnit2 @@ -232,11 +232,11 @@ let tests = "suite" >::: [ let _ = run_test_tt_main tests ``` -表达式 assert_raises exn (fun () -> e) 用于检查表达式 e 是否引发异常 exn 。如果是,则 OUnit 测试用例成功,否则失败。 +表达式 `assert_raises exn (fun () -> e)` 用于检查表达式 `e` 是否引发异常 `exn` 。如果是,则 `OUnit` 测试用例成功,否则失败。 -请注意, assert_raises 的第二个参数是类型为 unit -> 'a 的函数,有时被称为“thunk”。写一个这种类型的函数可能看起来很奇怪——唯一可能的输入是 () ——但这是函数式语言中常见的模式,用于暂停或延迟程序的评估。在这种情况下,我们希望 assert_raises 在准备好时评估 List.hd [] 。如果我们立即评估 List.hd [] , assert_raises 将无法检查是否引发了正确的异常。我们将在后面的章节中更多地了解 thunks。 +请注意, `assert_raises` 的第二个参数是类型为 `unit -> 'a` 的函数,有时被称为“`thun`k”。写一个这种类型的函数可能看起来很奇怪——唯一可能的输入是 `()` ——但这是函数式语言中常见的模式,用于暂停或延迟程序的评估。在这种情况下,我们希望 `assert_raises` 在准备好时评估 `List.hd []` 。如果我们立即评估 `List.hd []` , `assert_raises` 将无法检查是否引发了正确的异常。我们将在后面的章节中更多地了解 `thunks`。 > WARNING: > -> 常见的错误是忘记在 e 周围加上 (fun () -> ...) 。如果您犯了这个错误,程序可能仍然可以进行类型检查,但 OUnit 测试用例将失败:没有额外的匿名函数,异常会在 assert_raises 有机会处理它之前被引发。 +> 常见的错误是忘记在 `e` 周围加上 (`fun () -> ...`) 。如果您犯了这个错误,程序可能仍然可以进行类型检查,但` OUnit` 测试用例将失败:没有额外的匿名函数,异常会在 `assert_raises` 有机会处理它之前被引发。 diff --git a/src/ocaml_programming/ch3_options.md b/src/ocaml_programming/ch3_options.md index 2b86938..cf80116 100644 --- a/src/ocaml_programming/ch3_options.md +++ b/src/ocaml_programming/ch3_options.md @@ -2,7 +2,7 @@ -假设您想编写一个通常返回类型为 t 的值的函数,但有时不返回任何内容。例如,您可能想定义一个函数 list_max ,它返回列表中的最大值,但在空列表上没有明智的返回值: +假设您想编写一个通常返回类型为 `t` 的值的函数,但有时不返回任何内容。例如,您可能想定义一个函数 `list_max` ,它返回列表中的最大值,但在空列表上没有明智的返回值: ```ocaml let rec list_max = function @@ -12,22 +12,22 @@ let rec list_max = function 有几种可能性需要考虑: -- 返回 min_int ? 但这样 list_max 只能用于整数— 而不是浮点数或其他类型。 +- 返回 `min_int` ? 但这样 `list_max` 只能用于整数— 而不是浮点数或其他类型。 - 引发异常?但是函数的用户必须记得捕获异常。 -- 返回 null ? 这在 Java 中有效,但是按设计,OCaml 没有 null 值。这实际上是件好事:空指针错误不好调试。 +- 返回 `null` ? 这在 Java 中有效,但是按设计,OCaml 没有 `null` 值。这实际上是件好事:空指针错误不好调试。 > NOTE: > > 何瑞爵士称他发明的 null 为“[价值十亿美元的错误](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/)”。 -除了这些可能性之外,OCaml 还提供了一种更好的东西,称为选项。(Haskellers 将把选项识别为 Maybe 单子。) +除了这些可能性之外,OCaml 还提供了一种更好的东西,称为Option。(Haskellers 将把选项识别为 Maybe 单子。) 你可以把期权想象成一个封闭的盒子。也许盒子里面有东西,或者盒子是空的。在打开盒子之前,我们不知道里面是什么。如果当我们打开盒子时发现里面有东西,我们可以把那个东西拿出来并使用。因此,期权提供一种“或许类型”,最终是一种一种的类型:盒子处于两种状态之一,满或空。 -在上面的 list_max 中,我们想要用隐喻的方式返回一个空箱子,如果列表为空,或者如果列表非空,则返回一个包含列表中最大元素的箱子。 +在上面的 `list_max` 中,我们想要用隐喻的方式返回一个空箱子,如果列表为空,或者如果列表非空,则返回一个包含列表中最大元素的箱子。 -这是我们如何创建一个选项,就像一个里面有 42 的盒子: +这是我们如何创建一个选项,就像一个里面有 `42` 的盒子: ```ocaml Some 42 @@ -47,11 +47,11 @@ None - : 'a option = None ``` -Some 表示盒子里面有东西,而且是 42 。 None 表示盒子里面没有东西。 +`Some` 表示盒子里面有东西,而且是 `42` 。 `None` 表示盒子里面没有东西。 -像 list 一样,我们称 option 为类型构造器:给定一个类型,它会产生一个新类型;但是,它本身不是一个类型。因此对于任何类型 t ,我们可以将 t option 写成一个类型。但是 option 本身不能被用作一个类型。类型 t option 的值可能包含类型 t 的值,或者它们可能什么也不包含。 None 的类型是 'a option ,因为里面的东西的类型是不受限制的 — 因为里面什么也没有。 +像 `list` 一样,我们称 `option` 为类型构造器:给定一个类型,它会产生一个新类型;但是,它本身不是一个类型。因此对于任何类型 `t` ,我们可以将 `t option` 写成一个类型。但是 `option` 本身不能被用作一个类型。类型 `t option` 的值可能包含类型 `t` 的值,或者它们可能什么也不包含。 `None` 的类型是 `'a option` ,因为里面的东西的类型是不受限制的 — 因为里面什么也没有。 -您可以使用模式匹配访问选项值 e 的内容。以下是一个从选项中提取 int (如果存在)并将其转换为字符串的函数: +您可以使用模式匹配访问选项值 `e` 的内容。以下是一个从选项中提取 `int` (如果存在)并将其转换为字符串的函数: ```ocaml let extract o = @@ -79,7 +79,7 @@ extract None;; - : string = "" ``` -这是我们如何使用选项写 list_max 的方法: +这是我们如何使用选项写 `list_max` 的方法: ```ocaml let rec list_max = function @@ -97,12 +97,12 @@ val list_max : 'a list -> 'a option = > NOTE: > -> 上面嵌套模式匹配的 begin .. end 包装在这里并不是严格必需的,但也不是一个坏习惯,因为它将预防更复杂代码中的潜在语法错误。关键字 begin 和 end 等同于 ( 和 ) 。 +> 上面嵌套模式匹配的 `begin .. end` 包装在这里并不是严格必需的,但也不是一个坏习惯,因为它将预防更复杂代码中的潜在语法错误。关键字 `begin` 和 `end` 等同于 ( 和 ) 。 -在 Java 中,每个对象引用都隐式地是一个选项。要么引用中有一个对象,要么引用中什么都没有。那个“什么都没有”由值 null 表示。Java 不强制程序员显式检查空值情况,这会导致空指针异常。OCaml 选项强制程序员在模式匹配中包含一个分支 None ,从而确保程序员在没有内容时考虑正确的操作。因此,我们可以将选项视为一种有原则的方法,从语言中消除 null 。使用选项通常被认为是比引发异常更好的编码实践,因为它强制调用者在 None 情况下执行一些明智的操作。 +在 Java 中,每个对象引用都隐式地是一个选项。要么引用中有一个对象,要么引用中什么都没有。那个“什么都没有”由值 `null` 表示。Java 不强制程序员显式检查空值情况,这会导致空指针异常。OCaml 选项强制程序员在模式匹配中包含一个分支 `None` ,从而确保程序员在没有内容时考虑正确的操作。因此,我们可以将选项视为一种有原则的方法,从语言中消除 `null` 。使用选项通常被认为是比引发异常更好的编码实践,因为它强制调用者在 `None` 情况下执行一些明智的操作。 **选项的语法和语义。** -- t option 是每种类型 t 的一种类型。 -- None 是一种 'a option 类型的值。 -- Some e 是 t option 类型的表达式,如果 e : t 。如果 e ==> v ,那么 Some e ==> Some v 。 +- `t option` 是每种类型 `t` 的一种类型。 +- `None` 是一种 `'a option` 类型的值。 +- `Some e` 是 `t option` 类型的表达式,如果 `e : t` 。如果 `e ==> v` ,那么 `Some e ==> Some v` 。 diff --git a/src/ocaml_programming/ch3_records_and_tuples.md b/src/ocaml_programming/ch3_records_and_tuples.md index 69320c6..f75be57 100644 --- a/src/ocaml_programming/ch3_records_and_tuples.md +++ b/src/ocaml_programming/ch3_records_and_tuples.md @@ -22,7 +22,7 @@ type ptype = TNormal | TFire | TWater type mon = { name : string; hp : int; ptype : ptype; } ``` -这种类型定义了一个记录,其中包含三个字段,分别命名为 name , hp (生命值)和 ptype 。每个字段的类型也已给出。请注意, ptype 可以同时用作类型名称和字段名称;在 OCaml 中,它们的命名空间是不同的。 +这种类型定义了一个记录,其中包含三个字段,分别命名为 `name` , `hp` (生命值)和 `ptype` 。每个字段的类型也已给出。请注意, `ptype` 可以同时用作类型名称和字段名称;在 OCaml 中,它们的命名空间是不同的。 要构建记录类型的值,我们编写一个记录表达式,看起来像这样: @@ -53,7 +53,7 @@ val c : mon = {name = "Charmander"; hp = 39; ptype = TFire} 还可以使用模式匹配来访问记录字段: -``` +```ocaml match c with {name = n; hp = h; ptype = t} -> h ``` @@ -61,7 +61,7 @@ match c with {name = n; hp = h; ptype = t} -> h - : int = 39 ``` -这里的 n , h 和 t 是模式变量。如果您想要为字段和模式变量使用相同的名称,可以提供一种语法糖: +这里的 `n` , `h` 和 `t` 是模式变量。如果您想要为字段和模式变量使用相同的名称,可以提供一种语法糖: ```ocaml match c with {name; hp; ptype} -> hp @@ -71,7 +71,7 @@ match c with {name; hp; ptype} -> hp - : int = 39 ``` -这里,模式 {name; hp; ptype} 是 {name = name; hp = hp; ptype = ptype} 的糖。在这些子表达式中,出现在等号左侧的标识符是字段名,出现在右侧的标识符是模式变量。 +这里,模式 `{name; hp; ptype}` 是 `{name = name; hp = hp; ptype = ptype}` 的糖。在这些子表达式中,出现在等号左侧的标识符是字段名,出现在右侧的标识符是模式变量。 @@ -84,7 +84,7 @@ match c with {name; hp; ptype} -> hp {f1 = e1; ...; fn = en} ``` -记录表达式中 fi=ei 的顺序是无关紧要的。例如, {f = e1; g = e2} 完全等同于 {g = e2; f = e1} 。 +记录表达式中 `fi=ei` 的顺序是无关紧要的。例如, `{f = e1; g = e2}` 完全等同于 `{g = e2; f = e1}` 。 一个字段访问被写为: @@ -92,12 +92,12 @@ match c with {name; hp; ptype} -> hp e.f ``` -其中 f 必须是字段名称的标识符,而不是表达式。这个限制与其他具有类似特性的任何语言相同——例如,Java 字段名称。如果您真的想计算要访问的标识符,那么实际上您需要一个不同的数据结构:一个映射(也被许多其他名称称为:字典或关联列表或哈希表等,尽管每个术语都隐含着微妙的差异。) +其中 `f` 必须是字段名称的标识符,而不是表达式。这个限制与其他具有类似特性的任何语言相同——例如,Java 字段名称。如果您真的想计算要访问的标识符,那么实际上您需要一个不同的数据结构:一个映射(也被许多其他名称称为:字典或关联列表或哈希表等,尽管每个术语都隐含着微妙的差异。) **动态语义学** -- 如果对于 1..n 中的所有 i ,都成立 ei ==> vi ,那么 {f1 = e1; ...; fn = en} ==> {f1 = v1; ...; fn = vn} 。 -- 如果 e ==> {...; f = v; ...} ,那么 e.f ==> v 。 +- 如果对于 `1..n` 中的所有 `i` ,都成立 `ei ==> vi` ,那么 `{f1 = e1; ...; fn = en} ==> {f1 = v1; ...; fn = vn}` 。 +- 如果 `e ==> {...; f = v; ...}` ,那么 `e.f ==> v` 。 **静态语义学** @@ -107,15 +107,15 @@ e.f {f1 : t1; ...; fn : tn} ``` -记录类型中 fi:ti 的顺序是无关紧要的。例如, {f : t1; g : t2} 完全等同于 {g:t2;f:t1} 。 +记录类型中 `fi:ti` 的顺序是无关紧要的。例如, `{f : t1; g : t2}` 完全等同于 `{g:t2;f:t1}` 。 请注意,记录类型必须在使用之前定义。这使得 OCaml 能够进行比如果记录类型可以在没有定义的情况下使用时更好的类型推断。 类型检查规则如下: -- 如果对于 1..n 中的所有 i ,都满足 ei : ti ,并且如果 t 被定义为 {f1 : t1; ...; fn : tn} ,那么 {f1 = e1; ...; fn = en} : t 。请注意,记录表达式中提供的字段集必须是作为记录类型的一部分定义的完整字段集(但请参见下文有关记录复制的内容)。 +- 如果对于 `1..n` 中的所有 `i` ,都满足 `ei : ti` ,并且如果 `t` 被定义为 `{f1 : t1; ...; fn : tn}` ,那么 `{f1 = e1; ...; fn = en} : t` 。请注意,记录表达式中提供的字段集必须是作为记录类型的一部分定义的完整字段集(但请参见下文有关记录复制的内容)。 -- 如果 e : t1 ,并且如果 t1 被定义为 {...; f : t2; ...} ,那么 e.f : t2 。 +- 如果 `e : t1` ,并且如果 `t1` 被定义为 `{...; f : t2; ...}` ,那么 `e.f : t2`。 **记录复制** @@ -125,7 +125,7 @@ e.f {e with f1 = e1; ...; fn = en} ``` -这不会改变旧记录。相反,它会用新值构建一个新记录。在 with 之后提供的字段集合不必是作为记录类型的一部分定义的完整字段集合。在新复制的记录中,任何未作为 with 的一部分提供的字段都会从旧记录中复制过来。 +这不会改变旧记录。相反,它会用新值构建一个新记录。在 `with` 之后提供的字段集合不必是作为记录类型的一部分定义的完整字段集合。在新复制的记录中,任何未作为 `with` 的一部分提供的字段都会从旧记录中复制过来。 记录副本是一种语法糖。它相当于编写 @@ -134,19 +134,19 @@ e.f g1 = e.g1; ...; gn = e.gn } ``` -其中 gi 的集合是记录类型的所有字段的集合减去 fi 的集合。 +其中 `gi` 的集合是记录类型的所有字段的集合减去 `fi` 的集合。 ** 模式匹配。** 我们将以下新的模式形式添加到合法模式列表中: -- {f1 = p1; ...; fn = pn} +- `{f1 = p1; ...; fn = pn}` 我们将模式与值匹配并生成绑定的定义扩展如下: -- 如果对于 1..n 中的所有 i ,满足 pi 匹配 vi 并生成绑定 bi,那么记录模式 {f1 = p1; ...; fn = pn} 匹配记录值 {f1 = v1; ...; fn = vn; ...} 并生成绑定集 Uibi。请注意,记录值可能比记录模式具有更多的字段。 +- 如果对于 `1..n` 中的所有 `i` ,满足 `pi` 匹配 `vi` 并生成绑定 `bi`,那么记录模式 `{f1 = p1; ...; fn = pn}` 匹配记录值 `{f1 = v1; ...; fn = vn; ...}` 并生成绑定集 `Uibi`。请注意,记录值可能比记录模式具有更多的字段。 -作为一种语法糖,提供了另一种记录模式的形式: {f1; ...; fn} 。它被展开为 {f1 = f1; ...; fn = fn} 。 +作为一种语法糖,提供了另一种记录模式的形式: `{f1; ...; fn}` 。它被展开为 `{f1 = f1; ...; fn = fn}` 。 ## 元组 @@ -188,23 +188,23 @@ match (1, 2, 3) with (x, y, z) -> x + y + z **动态语义学** -- 如果对于 1..n 中的所有 i 都成立 ei ==> vi ,那么 (e1, ..., en) ==> (v1, ..., vn) 。 +- 如果对于 `1..n` 中的所有 `i` 都成立 `ei ==> vi` ,那么 `(e1, ..., en) ==> (v1, ..., vn)` 。 **静态语义学** -元组类型是使用一个新的类型构造函数 * 编写的,这与乘法运算符不同。类型 t1 * ... * tn 是第一个组件类型为 t1 ,...,第 n 个组件类型为 tn 的元组类型。 +元组类型是使用一个新的类型构造函数 `*` 编写的,这与乘法运算符不同。类型 `t1 * ... * tn` 是第一个组件类型为 `t1 ,...`,第 `n` 个组件类型为 `tn` 的元组类型。 -- 如果对于 1..n 中的所有 i 都成立 ei : ti ,那么 (e1, ..., en) : t1 * ... * tn 。 +- 如果对于 `1..n` 中的所有 `i` 都成立 `ei : ti` ,那么 `(e1, ..., en) : t1 * ... * tn` 。 **模式匹配** 我们将以下新的模式形式添加到合法模式列表中: -- (p1, ..., pn) +- `(p1, ..., pn)` 我们将模式与值匹配并生成绑定的定义扩展如下: -- 如果对于 1..n 中的所有 i ,都满足 pi 与 vi 匹配并产生绑定 bi,那么元组模式 (p1, ..., pn) 与元组值 (v1, ..., vn) 匹配并产生绑定集 Uibi。请注意,元组值的组件数量必须与元组模式的相同。 +- 如果对于 `1..n` 中的所有 `i` ,都满足 `pi` 与 `vi` 匹配并产生绑定 `bi`,那么元组模式 `(p1, ..., pn)` 与元组值 `(v1, ..., vn)` 匹配并产生绑定集 Uibi。请注意,元组值的组件数量必须与元组模式的相同。 ## 变体 vs. 元组和记录 @@ -216,6 +216,6 @@ match (1, 2, 3) with (x, y, z) -> x + y + z > > 上面的第二个视频使用了更高级的变体示例,这些将在[后面的部分](./ch3_algebraic_data_types.md)中学习。 -变体与我们刚学过的类型(记录和元组)之间的重要区别在于变体类型的值是一组可能性中的一个,而元组或记录类型的值提供一组可能性中的每一个。回到我们的例子,类型 day 的值是 Sun 或 Mon 或等等之一。但类型 mon 的值提供了 string 和 int 和 ptype 的每一个。请注意,在前两个句子中,单词“或”与变体类型相关联,而单词“和”与元组和记录类型相关联。这是一个很好的线索,如果你曾经试图决定是使用变体,还是元组或记录:如果你需要一个数据或另一个数据,你需要一个变体;如果你需要一个数据和另一个数据,你需要一个元组或记录。 +变体与我们刚学过的类型(记录和元组)之间的重要区别在于变体类型的值是一组可能性中的一个,而元组或记录类型的值提供一组可能性中的每一个。回到我们的例子,类型 `day` 的值是 `Sun` 或 `Mon` 或等等之一。但类型 `mon` 的值提供了 `string` 和 `int` 和 `ptype` 的每一个。请注意,在前两个句子中,单词“或”与变体类型相关联,而单词“和”与元组和记录类型相关联。这是一个很好的线索,如果你曾经试图决定是使用变体,还是元组或记录:如果你需要一个数据或另一个数据,你需要一个变体;如果你需要一个数据和另一个数据,你需要一个元组或记录。 -一对类型更常被称为和类型,每对类型被称为[积类型](https://en.wikipedia.org/wiki/Disjoint_union)。这些名称来自集合论。变体类似于不相交并集,因为变体的每个值来自许多基础集合中的一个(因此迄今为止,这些集合中的每一个仅仅是一个构造器,因此基数为一)。不相交并集有时确实用求和运算符来表示。元组/记录类似于[笛卡尔积](https://en.wikipedia.org/wiki/Cartesian_product),因为元组或记录的每个值包含来自许多基础集合中的一个值。笛卡尔积通常用乘法运算符 x 或 PI来表示。 +一对类型更常被称为和类型,每对类型被称为[积类型](https://en.wikipedia.org/wiki/Disjoint_union)。这些名称来自集合论。变体类似于不相交并集,因为变体的每个值来自许多基础集合中的一个(因此迄今为止,这些集合中的每一个仅仅是一个构造器,因此基数为一)。不相交并集有时确实用求和运算符来表示。元组/记录类似于[笛卡尔积](https://en.wikipedia.org/wiki/Cartesian_product),因为元组或记录的每个值包含来自许多基础集合中的一个值。笛卡尔积通常用乘法运算符 `x` 或 `PI`来表示。 diff --git a/src/ocaml_programming/ch3_type_synonyms.md b/src/ocaml_programming/ch3_type_synonyms.md index 7f3760a..6ac7dd5 100644 --- a/src/ocaml_programming/ch3_type_synonyms.md +++ b/src/ocaml_programming/ch3_type_synonyms.md @@ -20,7 +20,7 @@ type vector = float list type matrix = float list list ``` -无论何处需要 float * float ,您都可以使用 point ,反之亦然。这两者可以完全互换。在下面的代码中, get_x 不在乎您传递的是被注释为其中一个还是另一个的值: +无论何处需要 `float * float` ,您都可以使用 `point` ,反之亦然。这两者可以完全互换。在下面的代码中, `get_x` 不在乎您传递的是被注释为其中一个还是另一个的值: ```ocaml let get_x = fun (x, _) -> x