基本数据类型和模式匹配

先决条件

简介

本文档涵盖原子类型,如整数和布尔值;预定义复合类型,如字符串和列表;以及用户定义类型,即变体和记录。我们将展示如何对这些类型进行模式匹配。

在 OCaml 中,运行时没有类型检查,并且除非显式转换,否则值不会改变类型。这就是静态和强类型化的含义。这允许对结构化数据进行安全处理。

注意:与之前的教程一样,# 后面的表达式以 ;; 结尾,适用于顶级环境,例如 UTop。

预定义类型

整数、浮点数、布尔值和字符

整数

int 类型是 OCaml 中的默认基本整数类型。当您输入一个整数时,OCaml 会将其识别为整数,如以下示例所示

# 42;;
- : int = 42

int 类型表示平台相关的有符号整数。这意味着 int 的位数并不总是相同。它取决于底层平台的特性,例如处理器架构或操作系统。对 int 值的操作由 StdlibInt 模块提供。

通常,int 在 32 位架构中为 31 位,在 64 位架构中为 63 位,因为一位保留用于 OCaml 的运行时操作。标准库还提供了 Int32Int64 模块,它们支持对 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 的操作由 StdlibFloat 模块提供。

布尔值

布尔值由类型 bool 表示。

# true;;
- : bool = true

# false;;
- : bool = false

# false < true;;
- : bool = true

bool 的操作由 StdlibBool 模块提供。连接 “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 值的操作由 StdlibChar 模块提供。

Uchar 模块提供对 Unicode 字符的支持。

字符串和字节序列

字符串

字符串是不可变的,这意味着无法更改字符串中字符的值。

# "hello" ^ " " ^ "world!";;
- : string = "hello world!"

字符串是 char 值的有限固定大小序列。字符串连接运算符符号是 ^

可以使用以下语法对字符串字符进行索引访问

# "buenos dias".[4];;
- : char : 'o'

string 值的操作由 StdlibString 模块提供。

字节序列

# String.to_bytes "hello";;
- : bytes = Bytes.of_string "hello"

与字符串类似,字节序列是有限且固定大小的。每个字节由 char 值表示。与数组类似,字节序列是可变的,这意味着它们不能扩展或缩短,但每个组件字节都可以更新。本质上,字节序列(类型 bytes)是一个不可打印的可变字符串。没有办法写 bytes 文字,所以它们必须由函数生成。

bytes 值的操作由 StdlibBytes 模块提供。只有 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 arraychar arraystring array,但数组中可以使用任何类型的数据。通常,array 被称为多态类型。严格来说,它是一个类型运算符,它接受一个类型作为参数(这里为 intcharstring)来形成另一个类型(这里推断出的)。这是空数组。

# [||];;
- : 'a array = [||]

记住,'a(“alpha”)是一个类型参数,它将被另一个类型替换。

stringbytes 一样,数组支持直接访问,但语法不同。

# [| '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 类型可用于表示函数的结果可能是成功或失败。只有两种方法可以构建结果值:使用 OkError,它们具有预期的含义。两种构造函数都可以保存任何类型的数据。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 * charfloat * 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 * boolint * (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 必须小写。第一个省略号代表类型名称,并且不能以大写字母开头。第二个省略号代表类型定义。有三种情况可能

  1. 变体
  2. 记录
  3. 别名

接下来的三节将介绍这三种类型的类型定义。

变体

变体也称为 带标签的联合。它们与 不相交并集 的概念有关。

枚举数据类型

变体类型的最简单形式对应于 枚举类型。它由命名的值的显式列表定义。定义的值称为构造函数,并且必须大写。

例如,以下是如何定义变体数据类型来表示龙与地下城角色职业和阵营。

# 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 也是一种变体,它有两个不包含数据的构造函数:truefalse

包含数据的构造函数

可以在构造函数中包装数据。以下类型有几个包含数据的构造函数(例如,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有两个intbool类型的参数,而构造函数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

两个构造函数ArrayObject都包含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是一个具有两个构造函数的变体类型:SomeNone。它可以包含任何类型的值,例如Some 42Some "hola"。从这个意义上说,option是多态的。以下是它在标准库中的定义方式

# #show option;;
type 'a option = None | Some of 'a

预定义类型list在同一意义上是多态的。它是一个具有两个构造函数的变体,可以保存任何类型的数据。以下是它在标准库中的定义方式

# #show list;;
type 'a list = [] | (::) of 'a * 'a list

这里唯一的魔法是将构造函数变成符号,我们不会在本教程中介绍。类型boolunit也是常规变体,具有相同的魔法

# #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 * boolPair (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 有几种形式的多态性。在大多数情况下,这些概念之间的区别是模糊的,但有时有必要区分它们。

以下是适用于数据类型的术语

  1. 'a list'a option'a tree通常被称为多态类型。正式地说,bool listint option是类型,而listoption类型运算符,它们接受类型参数并产生类型。这是一种参数多态性'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 有其他类型系统,但这篇文档重点介绍了使用该算法类型化的数据。

仍然需要帮助?

帮助改进我们的文档

所有 OCaml 文档都是开源的。看到有错误或不清楚的地方吗?提交一个拉取请求。

OCaml

创新。社区。安全。