基本数据类型和模式匹配
简介
本文档涵盖原子类型,如整数和布尔值;预定义复合类型,如字符串和列表;以及用户定义类型,即变体和记录。我们将展示如何对这些类型进行模式匹配。
在 OCaml 中,运行时没有类型检查,并且除非显式转换,否则值不会改变类型。这就是静态和强类型化的含义。这允许对结构化数据进行安全处理。
注意:与之前的教程一样,#
后面的表达式以 ;;
结尾,适用于顶级环境,例如 UTop。
预定义类型
整数、浮点数、布尔值和字符
整数
int
类型是 OCaml 中的默认基本整数类型。当您输入一个整数时,OCaml 会将其识别为整数,如以下示例所示
# 42;;
- : int = 42
int
类型表示平台相关的有符号整数。这意味着 int
的位数并不总是相同。它取决于底层平台的特性,例如处理器架构或操作系统。对 int
值的操作由 Stdlib
和 Int
模块提供。
通常,int
在 32 位架构中为 31 位,在 64 位架构中为 63 位,因为一位保留用于 OCaml 的运行时操作。标准库还提供了 Int32
和 Int64
模块,它们支持对 32 位和 64 位有符号整数进行平台无关的操作。这些模块在本教程中没有详细说明。
OCaml 中没有专门的无符号整数类型。对 int
的按位运算将符号位与其他位相同对待。二元运算符使用标准符号。有符号余数运算符写为 mod
。OCaml 中的整数没有预定义的幂运算符。
浮点数和类型转换
浮点数具有类型 float
。
OCaml 不会对值执行任何隐式类型转换。因此,算术表达式不能混合整数和浮点数。参数要么全部为 int
,要么全部为 float
。浮点数上的算术运算符并不相同,它们用点后缀表示:+.
、-.
、*.
、/.
。
# let pi = 3.14159;;
val pi : float = 3.14159
# let tau = 2.0 *. pi;;
val tau : float = 6.28318
# let tau = 2 *. pi;;
Error: This expression has type int but an expression was expected of type
float
# let tau = 2 * pi;;
Error: This expression has type float but an expression was expected of type
int
对 float
的操作由 Stdlib
和 Float
模块提供。
布尔值
布尔值由类型 bool
表示。
# true;;
- : bool = true
# false;;
- : bool = false
# false < true;;
- : bool = true
对 bool
的操作由 Stdlib
和 Bool
模块提供。连接 “and” 写为 &&
,析取 “or” 写为 ||
。两者都是短路运算,这意味着如果左边的值足以决定整个表达式的值,它们不会计算右边的参数。
在 OCaml 中,if … then … else …
是一个条件表达式。它与它的分支具有相同的类型。
# 3 * if "foo" = "bar" then 5 else 5 + 2;;
- : int = 21
测试子表达式必须具有类型 bool
。分支子表达式必须具有相同的类型。
条件表达式和对布尔值的模式匹配相同
# 3 * match "foo" = "bar" with true -> 5 | false -> 5 + 2;;
- : int = 21
字符
类型 char
的值对应于 Latin-1 集中的 256 个符号。字符文字用单引号括起来,如下所示
# 'd';;
- : char = 'd'
对 char
值的操作由 Stdlib
和 Char
模块提供。
Uchar
模块提供对 Unicode 字符的支持。
字符串和字节序列
字符串
字符串是不可变的,这意味着无法更改字符串中字符的值。
# "hello" ^ " " ^ "world!";;
- : string = "hello world!"
字符串是 char
值的有限固定大小序列。字符串连接运算符符号是 ^
。
可以使用以下语法对字符串字符进行索引访问
# "buenos dias".[4];;
- : char : 'o'
对 string
值的操作由 Stdlib
和 String
模块提供。
字节序列
# String.to_bytes "hello";;
- : bytes = Bytes.of_string "hello"
与字符串类似,字节序列是有限且固定大小的。每个字节由 char
值表示。与数组类似,字节序列是可变的,这意味着它们不能扩展或缩短,但每个组件字节都可以更新。本质上,字节序列(类型 bytes
)是一个不可打印的可变字符串。没有办法写 bytes
文字,所以它们必须由函数生成。
bytes
值的操作由 Stdlib
和 Bytes
模块提供。只有 Bytes.get
函数允许直接访问字节序列中包含的字符。与数组不同,字节序列没有直接访问运算符。
bytes
的内存表示比 char array
紧凑四倍。
数组 & 列表
数组
数组是相同类型值的有限且固定大小的序列。以下是一些示例
# [| 0; 1; 2; 3; 4; 5 |];;
- : int array = [|0; 1; 2; 3; 4; 5|]
# [| 'x'; 'y'; 'z' |];;
- : char array = [|'x'; 'y'; 'z'|]
# [| "foo"; "bar"; "baz" |];;
- : string array = [|"foo"; "bar"; "baz"|]
数组可以包含任何类型的值。这里数组是 int array
、char array
和 string array
,但数组中可以使用任何类型的数据。通常,array
被称为多态类型。严格来说,它是一个类型运算符,它接受一个类型作为参数(这里为 int
、char
和 string
)来形成另一个类型(这里推断出的)。这是空数组。
# [||];;
- : 'a array = [||]
记住,'a
(“alpha”)是一个类型参数,它将被另一个类型替换。
与 string
和 bytes
一样,数组支持直接访问,但语法不同。
# [| 'x'; 'y'; 'z' |].(2);;
- : char = 'z'
数组是可变的,这意味着它们不能扩展或缩短,但每个元素都可以更新。
# let letter = [| 'v'; 'x'; 'y'; 'z' |];;
val letter : char array = [|'v'; 'x'; 'y'; 'z'|]
# letter.(2) <- 'F';;
- : unit = ()
# letter;;
- : char array = [|'v'; 'x'; 'F'; 'z'|]
左箭头 <-
是数组更新运算符。上面,它表示索引为 2 的单元格被设置为值 'F'
。它与编写 Array.set letter 2 'F'
相同。数组更新是一个副作用,并且返回单位值。
对数组的操作由 Array
模块提供。有一个专门的教程介绍 数组。
列表
作为字面量,列表非常类似于数组。以下是一些相同的示例,变成了列表。
# [ 0; 1; 2; 3; 4; 5 ];;
- : int list = [0; 1; 2; 3; 4; 5]
# [ 'x'; 'y'; 'z' ];;
- : char list = ['x'; 'y'; 'z']
# [ "foo"; "bar"; "baz" ];;
- : string list = ["foo"; "bar"; "baz"]
与数组一样,列表是相同类型值的有限序列。它们也是多态的。但是,列表是可扩展的、不可变的,并且不支持直接访问它们包含的所有值。列表在函数式编程中起着核心作用,因此它们有一个 专门的教程。
对列表的操作由 List
模块提供。List.append
函数连接两个列表。它可以用符号 @
作为运算符使用。
有一些对列表特别重要的符号
- 空列表写成
[]
,类型为'a list'
,发音为“nil”。 - 列表构造运算符,写成
::
,发音为“cons”,用于在列表开头添加一个值。
它们共同是构建列表和访问它存储的数据的基本手段。例如,以下是通过连续应用 cons (::
) 运算符来构建列表的方式
# 3 :: [];;
- : int list = [3]
# 2 :: 3 :: [];;
- : int list = [2; 3]
# 1 :: 2 :: 3 :: [];;
- : int list = [1; 2; 3]
模式匹配提供了访问存储在列表中的数据的基本手段。
# match [1; 2; 3] with
| x :: u -> x
| [] -> raise Exit;;
- : int = 1
# match [1; 2; 3] with
| x :: y :: u -> y
| x :: u -> x
| [] -> raise Exit;;
- : int = 2
在上面的表达式中,[1; 2; 3]
是要匹配的值。|
和 ->
符号之间的每个表达式都是一个模式。它们是类型为 list
的表达式,仅使用 []
、::
和代表列表可能具有的各种形状的绑定名称来形成。模式 []
表示“如果列表为空”。模式 x :: u
表示“如果列表包含数据,则让 x
为列表的第一个元素,而 u
为列表的其余部分”。->
符号右侧的表达式是每个对应情况下返回的结果。
选项 & 结果
选项
option
类型也是一种多态类型。选项值可以存储任何类型的数据或表示没有这样的数据。选项值只能通过两种不同的方式构建:None
,当没有数据可用时,或 Some
,否则。
# None;;
- : 'a option = None
# Some 42;;
- : int option = Some 42
# Some "hello";;
- : string option = Some "hello"
这是一个关于选项值的模式匹配示例
# match Some 42 with None -> raise Exit | Some x -> x;;
- : int = 42
对选项的操作由 Option
模块提供。选项在 错误处理 指南中讨论。
结果
result
类型可用于表示函数的结果可能是成功或失败。只有两种方法可以构建结果值:使用 Ok
或 Error
,它们具有预期的含义。两种构造函数都可以保存任何类型的数据。result
类型是多态的,但它有两个类型参数:一个用于 Ok
值,另一个用于 Error
值。
# Ok 42;;
- : (int, 'a) result = Ok 42
# Error "Sorry";;
- : ('a, string) result = Error "Sorry"
对结果的操作由 Result
模块提供。结果在 错误处理 指南中讨论。
元组
这是一个包含两个值的元组,也称为对。
# (3, 'K');;
- : int * char = (3, 'K')
该对包含整数 3
和字符 'K'
;它的类型为 int * char
。*
符号代表乘积类型。
这可以推广到包含 3 个或更多元素的元组。例如,(6.28, true, "hello")
的类型为 float * bool * string
。类型 int * char
和 float * bool * string
被称为乘积类型。*
符号用于表示捆绑在一起的产品中的类型。
预定义函数 fst
返回对的第一个元素,而 snd
返回对的第二个元素。
# fst (3, 'g');;
- : int = 3
# snd (3, 'g');;
- : char = 'g'
在标准库中,两者都使用模式匹配定义。以下是函数如何提取四种类型乘积的第三个元素
# let f x = match x with (h, i, j, k) -> j;;
val f : 'a * 'b * 'c * 'd -> 'c = <fun>
请注意,类型 int * char * bool
、int * (char * bool)
和 (int * char) * bool
并不相同。值 (42, 'h', true)
、(42, ('h', true))
和 ((42, 'h'), true)
不相等。用数学语言来说,乘积类型运算符 *
不是结合的。
函数
从类型 m
到类型 n
的函数类型写成 m -> n
。以下是一些示例
# fun x -> x * x;;
- : int -> int = <fun>
# (fun x -> x * x) 9;;
- : int = 81
第一个表达式是一个类型为 int -> int
的匿名函数。类型从表达式 x * x
推断出来,该表达式必须是类型 int
,因为 *
是一个返回 int
的运算符。<fun>
在值位置打印的标记是一个令牌,表示函数没有要显示的值。这是因为,如果它们已被编译,那么它们的代码将不再可用。
第二个表达式是函数应用。参数 9
被应用,结果 81
被返回。
# fun x -> x;;
- : 'a -> 'a = <fun>
# (fun x -> x) 42;;
- : int = 42
# (fun x -> x) "This is really disco!";;
- : string = "This is really disco!"
第一个表达式是另一个匿名函数。它是标识函数,它可以应用于任何东西,并且它返回其参数不变。这意味着它的参数可以是任何类型,并且它的结果具有相同的类型。相同的代码可以应用于不同类型的数据。这称为多态。
记住,'a
是一个类型参数,因此任何类型的数值都可以传递给函数,并且它们的类型会替换类型参数。标识函数具有相同的输入和输出类型,无论它是什么。
以下两个表达式表明标识函数可以应用于不同类型的参数
# let f = fun x -> x * x;;
val f : int -> int = <fun>
# f 9;;
- : int = 81
定义函数与命名值相同,如第一个表达式所示
# let g x = x * x;;
val g : int -> int = <fun>
# g 9;;
- : int = 81
可执行的 OCaml 代码主要由函数组成,因此使它们尽可能简洁明了是有益的。函数 g
在这里使用更短、更常见、可能更直观的语法定义。
在 OCaml 中,函数可以通过抛出异常(类型为 exn
)来终止,而不会返回预期的类型值,该异常不会出现在其类型中。无法在不检查函数代码的情况下知道函数是否可能引发异常。
# raise;;
- : exn -> 'a' = <fun>
异常在 错误处理 指南中讨论。
函数可以有多个参数。
# fun s r -> s ^ " " ^ r;;
- : string -> string -> string = <fun>
# let mean s r = (s + r) / 2;;
val mean : int -> int -> int = <fun>
与乘积类型符号 *
一样,函数类型符号 ->
也不结合。以下两种类型并不相同
(int -> int) -> int
: 此函数将类型为int -> int
的函数作为参数,并返回int
作为结果。int -> (int -> int)
: 此函数将int
作为参数,并返回类型为int -> int
的函数作为结果。
单位
唯一的是,unit
类型只有一个值。它写成 ()
,发音为“unit”。
unit
类型有几个用途。主要是在函数不需要传递数据或在完成计算后没有数据要返回时,它用作令牌。当函数具有副作用,例如操作系统级 I/O 时,就会发生这种情况。函数需要应用于某些东西才能触发它们的计算,并且它们还必须返回某些东西。当无法传递或返回有意义的东西时,应使用 ()
。
# read_line;;
- : unit -> string = <fun>
# print_endline;;
- : string -> unit = <fun>
函数 read_line
从标准输入读取以换行符结尾的字符序列,并将其作为字符串返回。当传递 ()
时,读取输入将开始。
# read_line ();;
foo bar
- : string = "foo bar"
# print_endline;;
- : string -> unit = <fun>
注意:将 foo bar
替换为您自己的文本,然后按 Return
。
函数 print_endline
将字符串打印到标准输出,并在后面加上换行符。返回单位值表示输出请求已被操作系统排队。
用户定义类型
用户定义类型总是使用 type … = …
语句引入。关键字 type
必须小写。第一个省略号代表类型名称,并且不能以大写字母开头。第二个省略号代表类型定义。有三种情况可能
- 变体
- 记录
- 别名
接下来的三节将介绍这三种类型的类型定义。
变体
枚举数据类型
变体类型的最简单形式对应于 枚举类型。它由命名的值的显式列表定义。定义的值称为构造函数,并且必须大写。
例如,以下是如何定义变体数据类型来表示龙与地下城角色职业和阵营。
# type character_class =
| Barbarian
| Bard
| Cleric
| Druid
| Fighter
| Monk
| Paladin
| Ranger
| Rogue
| Sorcerer
| Wizard;;
type character_class =
Barbarian
| Bard
| Cleric
| Druid
| Fighter
| Monk
| Paladin
| Ranger
| Rogue
| Sorcerer
| Wizard
# type rectitude = Evil | R_Neutral | Good;;
type rectitude = Evil | R_Neutral | Good
# type firmness = Chaotic | F_Neutral | Lawful;;
type firmness = Chaotic | F_Neutral | Lawful
这些类型的变体类型也可以用于表示工作日、基点或任何其他可以命名的固定大小的值集。值的排序按定义顺序定义(例如,Druid < Ranger
)。
可以对上面定义的类型执行模式匹配
# let rectitude_to_french = function
| Evil -> "Mauvais"
| R_Neutral -> "Neutre"
| Good -> "Bon";;
val rectitude_to_french : rectitude -> string = <fun>
请注意
unit
是一种变体,它只有一个构造函数,不包含数据:()
。bool
也是一种变体,它有两个不包含数据的构造函数:true
和false
。
包含数据的构造函数
可以在构造函数中包装数据。以下类型有几个包含数据的构造函数(例如,Hash of string
)和一些不包含数据的构造函数(例如,Head
)。它表示引用 Git 修订版 的不同方法。
# type commit =
| Hash of string
| Tag of string
| Branch of string
| Head
| Fetch_head
| Orig_head
| Merge_head;;
type commit =
Hash of string
| Tag of string
| Branch of string
| Head
| Fetch_head
| Orig_head
| Merge_head
以下是如何使用模式匹配将 commit
转换为 string
# let commit_to_string = function
| Hash sha -> sha
| Tag name -> name
| Branch name -> name
| Head -> "HEAD"
| Fetch_head -> "FETCH_HEAD"
| Orig_head -> "ORIG_HEAD"
| Merge_head -> "MERGE_HEAD";;
val commit_to_string : commit -> string = <fun>
上面,使用 function …
结构而不是之前使用的 match … with …
结构
let commit_to_string' x = match x with
| Hash sha -> sha
| Tag name -> name
| Branch name -> name
| Head -> "HEAD"
| Fetch_head -> "FETCH_HEAD"
| Orig_head -> "ORIG_HEAD"
| Merge_head -> "MERGE_HEAD";;
val commit_to_string' : commit -> string = <fun>
我们需要将经过检查的表达式传递给match … with …
结构。function …
是一种匿名函数的特殊形式,它接受一个参数并将其转发到match … with …
结构,如上所示。
警告:用括号将产品类型包装会将它们变成一个单一参数。
# type t =
| C1 of int * bool
| C2 of (int * bool);;
type t = C1 of int * bool | C2 of (int * bool)
# let p = (4, false);;
val p : int * bool = (4, false)
# C1 p;;
Error: The constructor C1 expects 2 argument(s),
but is applied here to 1 argument(s)
# C2 p;;
- : t = C2 (4, false)
构造函数C1
有两个int
和bool
类型的参数,而构造函数C2
有一个int * bool
类型的参数。
递归变体
引用自身的变体定义是递归的。构造函数可以包装来自被定义类型的數據。
对于以下定义,情况就是这样,该定义可用于存储 JSON 值。
# type json =
| Null
| Bool of bool
| Int of int
| Float of float
| String of string
| Array of json list
| Object of (string * json) list;;
type json =
Null
| Bool of bool
| Int of int
| Float of float
| String of string
| Array of json list
| Object of (string * json) list
两个构造函数Array
和Object
都包含json
类型的值。
使用递归变体上的模式匹配定义的函数通常也是递归的。此函数检查名称是否在整个 JSON 树中存在
# let rec has_field name = function
| Array u ->
List.fold_left (fun b obj -> b || has_field name obj) false u
| Object u ->
List.fold_left
(fun b (key, obj) -> b || key = name || has_field name obj) false u
| _ -> false;;
val has_field : string -> json -> bool = <fun>
这里,最后一个模式使用符号_
,它捕获所有内容。它对所有既不是Array
也不是Object
的数据返回false
。
多态数据类型
重新审视预定义类型
预定义类型option
是一个具有两个构造函数的变体类型:Some
和None
。它可以包含任何类型的值,例如Some 42
或Some "hola"
。从这个意义上说,option
是多态的。以下是它在标准库中的定义方式
# #show option;;
type 'a option = None | Some of 'a
预定义类型list
在同一意义上是多态的。它是一个具有两个构造函数的变体,可以保存任何类型的数据。以下是它在标准库中的定义方式
# #show list;;
type 'a list = [] | (::) of 'a * 'a list
这里唯一的魔法是将构造函数变成符号,我们不会在本教程中介绍。类型bool
和unit
也是常规变体,具有相同的魔法
# #show unit;;
type unit = ()
# #show bool;;
type bool = false | true
隐式地,产品类型也表现为变体类型。例如,对可以被视为该类型的成员
# type ('a, 'b) pair = Pair of 'a * 'b;;
type ('a, 'b) pair = Pair of 'a * 'b
(int, bool) pair
将写成int * bool
,Pair (42, true)
将写成(42, true)
。从开发人员的角度来看,一切都是发生在好像为每种可能的乘积形状声明了这种类型。这就是允许对乘积进行模式匹配的原因。
即使是整数和浮点数也可以被视为枚举类型的变体类型,具有许多构造函数和奇特的语法糖,它允许对这些类型进行模式匹配。
最终,唯一不简化为变体的类型构造是函数箭头类型。模式匹配允许检查任何类型的值的,除了函数。
用户定义的多态
这是一个将构造函数与数据、没有数据的构造函数、多态和递归结合在一起的变体类型示例
# type 'a tree =
| Leaf
| Node of 'a * 'a tree * 'a tree;;
type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree
它可以用来表示任意标记的二叉树。假设这样的树将用整数标记,以下是一种使用递归和模式匹配来计算其整数总和的可能方法。
# let rec sum = function
| Leaf -> 0
| Node (x, lft, rht) -> x + sum lft + sum rht;;
val sum : int tree -> int = <fun>
以下是如何在该类型中定义 map 函数
# let rec map f = function
| Leaf -> Leaf
| Node (x, lft, rht) -> Node (f x, map f lft, map f rht);;
val map : ('a -> 'b) -> 'a tree -> 'b tree = <fun>
在 OCaml 社区以及更广泛的功能性编程社区中,术语“多态”的使用非常宽泛。它被应用于以类似的方式处理各种类型的操作。从这个广义上讲,OCaml 的几个特性都是多态的。每个特性都使用特定形式的多态性,并且都有一个名称。总之,OCaml 有几种形式的多态性。在大多数情况下,这些概念之间的区别是模糊的,但有时有必要区分它们。
以下是适用于数据类型的术语
'a list
、'a option
和'a tree
通常被称为多态类型。正式地说,bool list
或int option
是类型,而list
和option
是类型运算符,它们接受类型参数并产生类型。这是一种参数多态性。'a list
和'a option
表示类型族,它们是通过将类型参数应用于运算符而创建的所有类型。
记录
记录类似于元组,因为它们将多个值捆绑在一起。在元组中,元素由它们在相应乘积类型中的位置识别。它们要么是第一个、第二个、第三个,要么在其他位置。在记录中,每个元素都有一个名称和一个值。这个名称-值对被称为字段。这就是为什么记录类型必须在使用之前声明的原因。
例如,以下是一个记录类型定义,用于部分表示龙与地下城角色职业。请注意,以下代码取决于本教程中前面的定义。请确保您已在枚举数据类型部分输入了定义。
# type character = {
name : string;
level : int;
race : string;
class_type : character_class;
alignment : firmness * rectitude;
armor_class : int;
};;
type character = {
name : string;
level : int;
race : string;
class_type : character_class;
alignment : firmness * rectitude;
armor_class : int;
}
character
类型的的值携带与该乘积的成员相同的数据:string * int * string * character_class * character_alignment * int
。
使用点表示法访问字段,如下所示
# let ghorghor_bey = {
name = "Ghôrghôr Bey";
level = 17;
race = "half-ogre";
class_type = Fighter;
alignment = (Chaotic, R_Neutral);
armor_class = -8;
};;
val ghorghor_bey : character =
{name = "Ghôrghôr Bey"; level = 17; race = "half-ogre";
class_type = Fighter; alignment = (Chaotic, R_Neutral); armor_class = -8}
# ghorghor_bey.alignment;;
- : firmness * rectitude = (Chaotic, R_Neutral)
# ghorghor_bey.class_type;;
- : character_class = Fighter
# ghorghor_bey.level;;
- : int = 17
为了构造一个新的记录,其中一些字段值已更改,而无需输入未更改的字段,我们可以使用记录更新语法,如下所示
# let togrev = { ghorghor_bey with name = "Togrev"; level = 20; armor_class = -6 };;
val togrev : character =
{name = "Togrev"; level = 20; race = "half-ogre"; class_type = Fighter;
alignment = (Chaotic, R_Neutral); armor_class = -6}
请注意,记录的行为类似于单一构造函数变体。这允许对它们进行模式匹配。
# match ghorghor_bey with { level; _ } -> level;;
- : int = 17
别名
类型别名
就像值一样,任何类型都可以被赋予一个名称。
# type latitude_longitude = float * float;;
type latitude_longitude = float * float
这主要用作文档手段或缩短长类型表达式。
函数参数别名
函数参数也可以使用模式匹配为元组和记录命名。
(* Tuples as parameters *)
# let tuple_sum (x, y) = x + y;;
val tuple_sum : int * int -> int = <fun>
# let f ((x, y) as arg) = tuple_sum arg;;
val f : int * int -> int = <fun>
(* Records as parameters *)
# type dummy_record = {a: int; b: int};;
type dummy_record = { a : int; b : int; }
# let record_sum ({a; b}: dummy_record) = a + b;;
val record_sum : dummy_record -> int = <fun>
# let f ({a;b} as arg) = record_sum arg;;
val f : dummy_record -> int = <fun>
这对于匹配参数的变体值很有用。
# let meaning_of_life = function Some _ as opt -> opt | None -> Some 42;;
val meaning_of_life : int option -> int option = <fun>
完整示例:数学表达式
本示例展示了如何表示简单的数学表达式,如n * (x + y)
,并将它们象征性地相乘得到n * x + n * y
# type expr =
| Plus of expr * expr (* a + b *)
| Minus of expr * expr (* a - b *)
| Times of expr * expr (* a * b *)
| Divide of expr * expr (* a / b *)
| Var of string (* "x", "y", etc. *);;
type expr =
Plus of expr * expr
| Minus of expr * expr
| Times of expr * expr
| Divide of expr * expr
| Var of string
表达式n * (x + y)
将写成
# let e = Times (Var "n", Plus (Var "x", Var "y"));;
val e : expr = Times (Var "n", Plus (Var "x", Var "y"))
以下是一个函数,它将Times (Var "n", Plus (Var "x", Var "y"))
打印出来,使其更像n * (x + y)
# let rec to_string = function
| Plus (e1, e2) -> "(" ^ to_string e1 ^ " + " ^ to_string e2 ^ ")"
| Minus (e1, e2) -> "(" ^ to_string e1 ^ " - " ^ to_string e2 ^ ")"
| Times (e1, e2) -> "(" ^ to_string e1 ^ " * " ^ to_string e2 ^ ")"
| Divide (e1, e2) -> "(" ^ to_string e1 ^ " / " ^ to_string e2 ^ ")"
| Var v -> v;;
val to_string : expr -> string = <fun>
我们可以写一个函数来将n * (x + y)
或(x + y) * n
形式的表达式相乘,为此我们将使用嵌套模式
# let rec distrib = function
| Times (e1, Plus (e2, e3)) ->
Plus (Times (distrib e1, distrib e2),
Times (distrib e1, distrib e3))
| Times (Plus (e1, e2), e3) ->
Plus (Times (distrib e1, distrib e3),
Times (distrib e2, distrib e3))
| Plus (e1, e2) -> Plus (distrib e1, distrib e2)
| Minus (e1, e2) -> Minus (distrib e1, distrib e2)
| Times (e1, e2) -> Times (distrib e1, distrib e2)
| Divide (e1, e2) -> Divide (distrib e1, distrib e2)
| Var v -> Var v;;
val distrib : expr -> expr = <fun>
以下是如何使用它
# e |> distrib |> to_string |> print_endline;;
((n * x) + (n * y))
- : unit = ()
前两个模式是distrib
函数工作原理的关键。第一个模式是Times (e1, Plus (e2, e3))
,它匹配e1 * (e2 + e3)
形式的表达式。该第一个模式的右侧等效于(e1 * e2) + (e1 * e3)
。第二个模式做同样的事情,除了(e1 + e2) * e3
形式的表达式。
其余模式不会改变表达式的形式,但它们会对子表达式递归调用distrib
函数。这确保了所有子表达式也被乘出来。(如果你只想将表达式的最顶层乘出来,你可以用简单的e -> e
规则替换所有剩余的模式。)
反向操作,即分解出共同的子表达式,可以以类似的方式实现。以下版本只适用于顶层表达式。
# let top_factorise = function
| Plus (Times (e1, e2), Times (e3, e4)) when e1 = e3 ->
Times (e1, Plus (e2, e4))
| Plus (Times (e1, e2), Times (e3, e4)) when e2 = e4 ->
Times (Plus (e1, e3), e4)
| e -> e;;
val top_factorise : expr -> expr = <fun>
# top_factorise (Plus (Times (Var "n", Var "x"),
Times (Var "n", Var "y")));;
- : expr = Times (Var "n", Plus (Var "x", Var "y"))
上面的 factorise 函数引入了另一个特性:每个模式的守卫。条件紧随when
之后,这意味着只有在模式匹配并且when
子句中的条件满足时才会执行返回值。
结论
本教程全面概述了 OCaml 的基本数据类型及其用法。我们探讨了内置类型,如整数、浮点数、字符、列表、元组和字符串,以及用户定义类型:记录和变体。记录和元组是将异构数据分组到内聚单元中的机制。变体是将异构数据作为一致的替代方案公开的机制。
在本教程中,变体和乘积被介绍了,这对应于代数数据类型。在这个级别,使用了名义类型检查算法。从历史上看,这是 OCaml 的第一个类型系统,因为它来自 OCaml 的祖先ML 编程语言。尽管 OCaml 有其他类型系统,但这篇文档重点介绍了使用该算法类型化的数据。