OCaml 概览

本教程介绍 OCaml 的基本特性:值、表达式、列表、函数、模式匹配等等。

不需要任何 OCaml 或函数式编程知识;但假设读者具备一些基本的软件开发知识。请确保您已安装 OCaml 并设置了环境,如“安装 OCaml”页面所述。

建议您执行我们提供的示例,并对其进行实验,以感受使用 OCaml 编程的感觉。为此,您可以使用 UTop(通用顶层)。

UTop 允许用户通过读取和评估 OCaml 短语(如表达式或值定义)并将其结果打印到屏幕上来与 OCaml 交互。使用 utop 命令运行 UTop。按 Ctrl+D 退出。有关更多信息,您可以阅读“OCaml 顶层介绍”。

本概览中的一些示例包含注释。OCaml 中的注释以 (* 开头,以 *) 结尾,可以嵌套。由于它们会被 OCaml 忽略,因此可以在允许使用空格的任何地方使用它们。在将以下代码输入 UTop 时,可以省略注释。以下是一些示例

(* Here is a comment *)
(* Outside of the nested comment is still a comment. (* Here is a nested comment *) Outside of the nested comment again. *)
# 50 + (* A comment in between parts of an expression *) 50;;
- : int = 100

表达式和定义

让我们从一个简单的表达式开始

# 50 * 50;;
- : int = 2500

在 OCaml 中,所有事物都有一个值,每个值都有一个类型。上面的示例表示:“50 * 50 是一个类型为 int(整数)的表达式,其结果为 2500。” 由于它是一个匿名表达式,因此使用字符 - 来代替名称。

末尾的双分号 ;; 表示告诉顶层评估并打印给定短语的结果。

以下是其他基本值和类型的示例

# 6.28;;
- : float = 6.28

# "This is really disco!";;
- : string = "This is really disco!"

# 'a';; (* Note the single quotes *)
- : char = 'a'

# true;;
- : bool = true

OCaml 具有类型推断。它会自动确定表达式的类型,而无需程序员提供太多指导。列表具有专门的教程。暂时,以下两个表达式都是列表。前者包含整数,后者包含字符串。

# let u = [1; 2; 3; 4];;
val u : int list = [1; 2; 3; 4]

# ["this"; "is"; "mambo"];;
- : string list = ["this"; "is"; "mambo"]

列表的类型 int liststring list 是从其元素的类型推断出来的。列表可以为空 [](读作“nil”)。请注意,第一个列表使用 let … = … 结构被赋予了一个名称,该结构将在下面详细介绍。列表上最基本的操作是在现有列表的前面添加一个新元素。这使用“cons”运算符完成,用双冒号运算符 :: 表示。

# 9 :: u;;
- : int list = [9; 1; 2; 3; 4]

在 OCaml 中,if … then … else … 不是语句,而是表达式。

# 2 * if "hello" = "world" then 3 else 5;;
- : int = 10

if 开始到 5 结束的源代码被解析为一个单一的整数表达式,该表达式乘以 2。OCaml 无需两种不同的测试结构。 三元条件运算符if … then … else … 是相同的。另请注意,此处不需要括号,这在 OCaml 中很常见。

可以使用 let 关键字为值命名。这称为将值绑定到名称。例如

# let x = 50;;
val x : int = 50

# x * x;;
- : int = 2500

输入 let x = 50;; 时,OCaml 会响应 val x : int = 50,这意味着 x 是一个标识符,绑定到值 50。因此,x * x;; 的结果与 50 * 50;; 相同。

OCaml 中的绑定是不可变的,这意味着分配给名称的值永远不会改变。尽管 x 通常被称为变量,但实际上并非如此。它实际上是一个常量。使用过于简化的但可接受的语言,所有变量在 OCaml 中都是不可变的。可以为可以更新的值命名。在 OCaml 中,这称为引用,将在使用可变状态部分进行讨论。

OCaml 中没有重载,因此在词法范围内,名称具有单个值,该值仅取决于其定义。

在名称中不要使用连字符,而应使用下划线。例如:x_plus_y 可用,x-plus-y 不可用。

绑定可以被赋予特殊注释(有时称为“docstring”,文档字符串),编辑器和工具将其视为与绑定相关的注释。这些注释通过在注释开头添加第二个 * 来表示。例如

(** Feet in a mile *)
let feets = 5280;;
val feets : int = 5280

这将在odoc 作者指南:特殊注释中进一步讨论。

可以使用 let … = … in … 语法在表达式内部定义本地名称

# let y = 50 in y * y;;
- : int = 2500

# y;;
Error: Unbound value y

此示例定义了名称 y 并将其绑定到值 50。然后在表达式 y * y 中使用它,得到值 2500。请注意,y 仅在 in 关键字后的表达式中定义。

由于 let … = … in … 是一个表达式,因此它可以在另一个表达式中使用,以便为多个值设置自己的名称

# let a = 1 in
  let b = 2 in
    a + b;;
- : int = 3

这定义了两个名称:a 的值为 1b 的值为 2。然后,此示例在表达式 a + b 中使用它们,得到值 3

在 OCaml 中,等号有两个含义。它用于定义和相等性测试。

# let dummy = "hi" = "hello";;
val dummy : bool = false

这被解释为:“定义 dummy 为字符串 "hi""hello" 之间结构相等性测试的结果。” OCaml 还具有双等号运算符 ==,表示物理相等性,但本教程中不使用它。运算符 <>= 的否定,而 !=== 的否定。

函数

在 OCaml 中,由于所有事物都是值,因此函数也是值。函数使用 let 关键字定义

# let square x = x * x;;
val square : int -> int = <fun>

# square 50;;
- : int = 2500

此示例定义了一个名为 square 的函数,它具有单个参数 x。它的函数体是表达式 x * x。OCaml 中没有“return”关键字。

当将 square 应用于 50 时,它会将 x * x 评估为 50 * 50,得到 2500

REPL 指示 square 的类型为 int -> int。这意味着它是一个函数,接受一个 int 作为参数(输入)并返回一个 int 作为结果(输出)。函数值无法显示,因此打印的是 <fun>

# String.ends_with;;
- : suffix:string -> string -> bool = <fun>

# String.ends_with ~suffix:"less" "stateless";;
- : bool = true

一些函数,例如 String.ends_with,具有带标签的参数。当函数具有多个相同类型的参数时,标签很有用;命名参数可以让您推测它们的用途。在上面,~suffix:"less" 指示将 "less" 作为带标签的参数 suffix 传递。带标签的参数将在带标签的参数教程中详细介绍。

匿名函数

匿名函数没有名称,它们使用 fun 关键字定义

# fun x -> x * x;;
- : int -> int = <fun>

我们可以编写匿名函数并立即将它们应用于值

# (fun x -> x * x) 50;;
- : int = 2500

具有多个参数和部分应用的函数

函数可以具有多个参数,用空格隔开。

# let cat a b = a ^ " " ^ b;;
val cat : string -> string -> string = <fun>

函数 cat 具有两个 string 参数 ab,并返回一个类型为 string 的值。

# cat "ha" "ha";;
- : string = "ha ha"

函数并不需要用所有预期参数来调用。例如,可以只将 `a` 传递给 `cat` 而不传递 `b`。

# let cat_hi = cat "hi";;
val cat_hi : string -> string = <fun>

这将返回一个期望单个字符串的函数,这里是指 `cat` 定义中的 `b`。这被称为部分应用。在上面的例子中,`cat` 被部分应用于 `"hi"`。

从 `cat` 的部分应用得到的函数 `cat_hi` 的行为如下:

# cat_hi "friend";;
- : string = "hi friend"

类型参数和高阶函数

函数可以将函数作为参数,这被称为高阶函数。一个众所周知的例子是 `List.map`。以下是它的用法:

# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>

# List.map (fun x -> x * x);;
- : int list -> int list = <fun>

# List.map (fun x -> x * x) [0; 1; 2; 3; 4; 5];;
- : int list = [0; 1; 4; 9; 16; 25]

这个函数的名字以 `List.` 开头,因为它属于对列表进行操作的预定义函数库的一部分。这个事项将在后面详细讨论。函数 `List.map` 有两个参数:第二个参数是一个列表,第一个参数是一个可以应用于列表元素的函数,无论它们是什么。`List.map` 返回一个列表,该列表是通过将作为参数提供的函数应用于输入列表的每个元素形成的。

函数 `List.map` 可以应用于任何类型的列表。这里给它一个整数列表,但它可以是浮点数列表、字符串列表或任何东西。这被称为多态。`List.map` 函数是多态的,这意味着它有两个隐式的类型变量:`'a` 和 `'b`(读作“alpha”和“beta”)。它们都可以是任何类型;但是,关于传递给 `List.map` 的函数,

  1. 输入列表元素与其输入的类型相同。
  2. 输出列表元素与其输出的类型相同。

副作用和 `unit` 类型

执行操作系统级别的输入输出操作是使用函数完成的。以下是一个例子:

# read_line;;
- : unit -> string = <fun>

# read_line ();;
caramba
- : string = "caramba"

# print_endline;;
- : string -> unit = <fun>

# print_endline "¿Cuándo se come aquí?";;
¿Cuándo se come aquí?
- : unit = ()

函数 `read_line` 从标准输入读取字符,并在遇到行尾 (EOL) 时将它们作为字符串返回。函数 `print_endline` 将字符串打印到标准输出,后面跟着一个 EOL。

函数 `read_line` 不需要任何数据来执行,函数 `print_endline` 也没有任何有意义的数据要返回。表示这种数据缺失的是 `unit` 类型,它出现在它们的签名中。`unit` 类型只有一个值,写成 `()`,读作“unit”。当没有数据被传递或返回时,它用作占位符,但仍然必须传递一些标记来启动处理或表示处理已结束。

输入输出是执行函数时发生的但没有出现在函数类型中的一个例子。这被称为副作用,并不局限于 I/O。`unit` 类型通常用于表示副作用的存在,虽然并不总是这样。

递归函数

递归函数在自己的主体中调用自身。此类函数必须使用 `let rec … = …` 来声明,而不是仅仅使用 `let`。递归不是在 OCaml 中执行迭代计算的唯一方法。循环,如 `for` 和 `while`,是可用的,但它们旨在与可变数据一起使用,用于编写命令式 OCaml。否则,应该优先使用递归函数。

以下是一个创建两个边界之间连续整数列表的函数示例:

# let rec range lo hi =
    if lo > hi then
      []
    else
      lo :: range (lo + 1) hi;;
val range : int -> int -> int list = <fun>

# range 2 5;;
- : int list = [2; 3; 4; 5]

如其类型 `int -> int -> int list` 所示,函数 `range` 接受两个整数作为参数并返回一个整数列表作为结果。第一个 `int` 参数 `lo` 是范围的下界;第二个 `int` 参数 `hi` 是上界。如果 `lo > hi`,则返回空范围。这是 `if … then … else` 表达式的第一个分支。否则,将 `lo` 值附加到通过调用自身 `range` 创建的列表前面;这是递归。使用 `::`,即 OCaml 中的连接运算符,来实现附加。它通过在现有列表前面添加元素来构建一个新列表。在每次调用中都会取得进展;由于 `lo` 刚刚被附加到列表头部,因此 `range` 被调用为 `lo + 1`。这可以这样可视化(这不是 OCaml 语法):

   range 2 5
=> 2 :: range 3 5
=> 2 :: 3 :: range 4 5
=> 2 :: 3 :: 4 :: range 5 5
=> 2 :: 3 :: 4 :: 5 :: range 6 5
=> 2 :: 3 :: 4 :: 5 :: []
=> [2; 3; 4; 5]

每个 `=>` 符号对应于一个递归步骤的计算,除了最后一个。OCaml 内部处理列表,如倒数第二个表达式所示,但将它们显示为最后一个表达式。这只是美化打印。在最后两个步骤之间没有进行任何计算。

数据和类型

类型转换和类型推断

OCaml 有类型为 `float` 的浮点数。要添加浮点数,必须使用 `+.` 而不是 `+`。

# 2.0 +. 2.0;;
- : float = 4.

在 OCaml 中,`+.` 是浮点数之间的加法,而 `+` 是整数之间的加法。

在许多编程语言中,值可以自动从一种类型转换为另一种类型。这包括隐式类型转换提升。例如,在这样的语言中,如果编写 `1 + 2.5`,第一个参数(一个整数)将被提升为一个浮点数,从而使结果也成为一个浮点数。

OCaml 永远不会隐式地将值从一种类型转换为另一种类型。无法执行浮点数和整数的加法。以下两个例子都会抛出错误:

# 1 + 2.5;;
Error: This expression has type float but an expression was expected of type
         int

# 1 +. 2.5;;
Error: This expression has type int but an expression was expected of type
         float
  Hint: Did you mean `1.'?

在第一个例子中,`+` 旨在与整数一起使用,因此它不能与 `2.5` 浮点数一起使用。在第二个例子中,`+.` 旨在与浮点数一起使用,因此它不能与 `1` 整数一起使用。

在 OCaml 中,需要使用 `float_of_int` 函数将整数显式转换为浮点数。

# float_of_int 1 +. 2.5;;
- : float = 3.5

OCaml 需要显式转换的原因有很多。最重要的是,它使类型能够自动推导出来。OCaml 的类型推断算法为每个表达式计算一个类型,并且与其他语言相比,它需要很少的注释。可以说,这节省了比我们因更显式而损失的更多时间。

列表

列表可能是 OCaml 中最常见的类型。它们是有序的集合,包含具有相同类型的值。以下是一些例子。

# [];;
- : 'a list = []

# [1; 2; 3];;
- : int list = [1; 2; 3]

# [false; false; true];;
- : bool list = [false; false; true]

# [[1; 2]; [3]; [4; 5; 6]];;
- : int list list = [[1; 2]; [3]; [4; 5; 6]]

上面的例子可以这样理解:

  1. 空列表,nil
  2. 包含数字 1、2 和 3 的列表
  3. 包含布尔值 `false`、`false` 和 `true` 的列表。允许重复。
  4. 列表的列表

列表被定义为要么为空,写成 `[]`,要么是元素 `x` 附加在另一个列表 `u` 的前面,写成 `x :: u`(双冒号运算符读作“连接”)。

# 1 :: [2; 3; 4];;
- : int list = [1; 2; 3; 4]

在 OCaml 中,模式匹配提供了一种检查任何类型数据的方法,除了函数。在本节中,它是在列表上介绍的,并将推广到下一节的其他数据类型。以下是使用模式匹配来定义一个递归函数,该函数计算整数列表的总和:

# let rec sum u =
    match u with
    | [] -> 0
    | x :: v -> x + sum v;;
val sum : int list -> int = <fun>

# sum [1; 4; 3; 2; 5];;
- : int = 15

列表上的多态函数

以下是编写一个递归函数来计算列表长度的方法:

# let rec length u =
    match u with
    | [] -> 0
    | _ :: v -> 1 + length v;; (* _ doesn't define a name; it can't be used in the body *)
val length : 'a list -> int = <fun>

# length [1; 2; 3; 4];;
- : int = 4

# length ["cow"; "sheep"; "cat"];;
- : int = 3

# length [[]];;
- : int = 1

这个函数不仅作用于整数列表,而且作用于任何类型的列表。它是一个多态函数。它的类型表明输入类型为 `'a list`,其中 `'a` 是一个类型变量,代表任何类型。空列表模式 `[]` 可以是任何元素类型。因此,`_ :: v` 模式,作为列表头部的值,是无关紧要的,因为 `_` 模式表明它没有被检查。由于两种模式必须具有相同的类型,因此类型推断算法推断出 `'a list -> int` 类型。

定义一个高阶函数

可以将函数作为参数传递给另一个函数。具有其他函数作为参数的函数被称为高阶函数。这在前面使用 `List.map` 函数进行了说明。以下是使用列表上的模式匹配编写 `map` 的方法。

# let square x = x * x;;
val square : int -> int

# let rec map f u =
    match u with
    | [] -> []
    | x :: u -> f x :: map f u;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>

# map square [1; 2; 3; 4;];;
- : int list = [1; 4; 9; 16]

模式匹配,续

模式匹配不限于列表。可以使用它检查任何类型的数据,除了函数。模式是与被检查值进行比较的表达式。它可以使用 `if … then … else …` 来执行,但模式匹配更方便。以下是一个使用 `option` 数据类型的示例,将在模块和标准库部分详细介绍。

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

# let f opt = match opt with
    | None -> None
    | Some None -> None
    | Some (Some x) -> Some x;;
val f : 'a option option-> 'a option = <fun>

被检查的值是类型为 `option` 的 `opt`。它与从上到下的模式进行比较。如果 `opt` 是 `None` 选项,则它与第一个模式匹配。如果 `opt` 是 `Some None` 选项,则它与第二个模式匹配。如果 `opt` 是一个双重包装的选项,带有值,则它与第三个模式匹配。模式可以引入名称,就像 `let` 一样。在第三个模式中,`x` 指定了双重包装的选项内部的数据。

模式匹配在基本数据类型教程以及每个数据类型教程中都有详细介绍。

在这个其他例子中,使用 `if … then … else …` 和模式匹配进行了相同的比较。

# let g x =
  if x = "foo" then 1
  else if x = "bar" then 2
  else if x = "baz" then 3
  else if x = "qux" then 4
  else 0;;
val g : string -> int = <fun>

# let g' x = match x with
    | "foo" -> 1
    | "bar" -> 2
    | "baz" -> 3
    | "qux" -> 4
    | _ -> 0;;
val g' : string -> int = <fun>

下划线符号是通配模式;它与任何东西都匹配。

请注意,当模式匹配没有捕获所有情况时,OCaml 会抛出一个警告。

# fun i -> match i with 0 -> 1;;
Line 1, characters 9-28:
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
1
- : int -> int = <fun>

对和元组

元组是固定长度的集合,包含任何类型的元素。对是具有两个元素的元组。以下是一个 3 元组和一对。

# (1, "one", 'K');;
- : int * string * char = (1, "one", 'K')

# ([], false);;
- : 'a list * bool = ([], false)

访问元组的组件是通过模式匹配完成的。例如,预定义的函数 `snd` 返回对的第二个组件。

# let snd p =
    match p with
    | (_, y) -> y;;
val snd : 'a * 'b -> 'b = <fun>

# snd (42, "apple");;
- : string = "apple"

注意:`snd` 函数是在 OCaml 标准库中预定义的。

元组的类型是用 `*` 在组件类型之间写成的。

变体类型

就像模式匹配推广了 `switch` 语句一样,变体类型推广了枚举类型和联合类型。

以下是用作枚举数据类型的变体类型的定义:

# type primary_colour = Red | Green | Blue;;
type primary_colour = Red | Green | Blue

# [Red; Blue; Red];;
- : primary_colour list = [Red; Blue; Red]

以下是用作联合类型的变体类型的定义:

# type http_response =
    | Data of string
    | Error_code of int;;
type http_response = Data of string | Error_code of int

# Data "<!DOCTYPE html>
<html lang=\"en\">
  <head>
    <meta charset=\"utf-8\">
    <title>Dummy</title>
  </head>
  <body>
    Dummy Page
  </body>
</html>";;

- : http_response =
Data
 "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Dummy</title>\n  </head>\n  <body>\n    Dummy Page\n  </body>\n</html>"

# Error_code 404;;
- : http_response = Error_code 404

以下是介于两者之间的东西:

# type page_range =
    | All
    | Current
    | Range of int * int;;
type page_range = All | Current | Range of int * int

在前面的定义中,大写的标识符被称为构造函数。它们允许创建变体值。这与面向对象编程无关。

正如本节第一句话所暗示的那样,变体与模式匹配一起使用。以下是一些例子:

# let colour_to_rgb colour =
    match colour with
    | Red -> (0xff, 0, 0)
    | Green -> (0, 0xff, 0)
    | Blue -> (0, 0, 0xff);;
val colour_to_rgb : primary_colour -> int * int * int = <fun>

# let http_status_code response =
    match response with
    | Data _ -> 200
    | Error_code code -> code;;
val http_status_code : http_response -> int = <fun>

# let is_printable page_count cur range =
    match range with
    | All -> true
    | Current -> 0 <= cur && cur < page_count
    | Range (lo, hi) -> 0 <= lo && lo <= hi && hi < page_count;;
val is_printable : int -> int -> page_range -> bool = <fun>

像函数一样,如果变体在自己的定义中引用自身,它可以是递归的。预定义类型 `list` 提供了这种变体的示例。

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

如前所述,`sum`、`length` 和 `map` 函数提供了对列表变体类型进行模式匹配的示例。

记录

与元组类似,记录也打包了多种类型的元素。但是,每个元素都给定一个名称。与变体类型一样,记录类型必须在使用之前定义。以下是一些记录类型的示例,一个值,访问一个组件,以及对同一个记录进行模式匹配。

# type person = {
    first_name : string;
    surname : string;
    age : int
  };;
type person = { first_name : string; surname : string; age : int; }

# let gerard = {
     first_name = "Gérard";
     surname = "Huet";
     age = 76
  };;
val gerard : person = {first_name = "Gérard"; surname = "Huet"; age = 76}

定义gerard时,不需要声明类型。类型检查器会查找一个记录,该记录恰好包含三个具有匹配名称和类型的字段。注意,记录之间没有类型关系。无法声明扩展另一个记录类型的记录类型,方法是添加字段。如果找到完全匹配项,记录类型搜索将成功,否则将失败。

# let s = gerard.surname;;
val s : string = "Huet"

# let is_teenager person =
    match person with
    | { age = x; _ } -> 13 <= x && x <= 19;;
val is_teenager : person -> bool = <fun>

# is_teenager gerard;;
- : bool = false

这里,模式{ age = x; _ } 的类型为最近声明的包含类型为intage 字段的记录类型。类型int 从表达式13 <= x && x <= 19 推断出来。函数is_teenager 只适用于找到的记录类型,这里是person

处理错误

异常

当计算被打断时,会抛出异常。例如

# 10 / 0;;
Exception: Division_by_zero.

使用raise 函数抛出异常。

# let id_42 n = if n <> 42 then raise (Failure "Sorry") else n;;
val id_42 : int -> int = <fun>

# id_42 42;;
- : int = 42

# id_42 0;;
Exception: Failure "Sorry".

注意,异常不会出现在函数类型中。

使用try … with … 结构捕获异常

# try id_42 0 with Failure _ -> 0;;
- : int = 0

标准库提供了几个预定义的异常。可以定义异常。

使用result 类型

在 OCaml 中,处理错误的另一种方法是返回类型为result 的值,它可以表示正确的结果或错误。以下是它的定义方式

# #show result;;
type ('a, 'b) result = Ok of 'a | Error of 'b

因此,可以这样写

# let id_42_res n = if n <> 42 then Error "Sorry" else Ok n;;
val id_42_res : int -> (int, string) result = <fun>

# id_42_res 42;;
- : (int, string) result = Ok 42

# id_42_res 0;;
- : (int, string) result = Error "Sorry"

# match id_42_res 0 with
  | Ok n -> n
  | Error _ -> 0;;
- : int = 0

使用可变状态

OCaml 支持命令式编程。通常,let … = … 语法不会定义变量,而是定义常量。但是,OCaml 中存在可变变量。它们被称为引用。以下是创建整数引用的方法

# let r = ref 0;;
val r : int ref = {contents = 0}

在语法上,无法创建未初始化的引用或空引用。r 引用被初始化为整数零。使用! 解引用运算符访问引用的内容。

# !r;;
- : int = 0

注意,!rr 的类型不同:分别为intint ref。就像无法对整数和浮点数执行乘法一样,也无法更新整数或乘以引用。

让我们更新r 的内容。这里:= 是赋值运算符;它发音为“接收”。

# r := 42;;
- : unit = ()

它返回(),因为更改引用的内容是一个副作用。

# !r;;
- : int = 42

使用; 运算符在另一个表达式之后执行表达式。写a; b 表示:执行a。完成后,执行b,只返回b 的值。

# let text = ref "hello ";;
val text : string ref = {contents = "hello "}

# print_string !text; text := "world!"; print_endline !text;;
hello world!
- : unit = ()

以下是第二行发生的副作用

  1. 在标准输出上显示引用text 的内容
  2. 更新引用text 的内容
  3. 在标准输出上显示引用text 的内容

这种行为与命令式语言中的行为相同。但是,虽然; 未定义为函数,但它的行为就像类型为unit -> unit -> unit 的函数一样。

模块和标准库

在 OCaml 中,使用名为模块的东西来组织源代码。模块是定义的集合。标准库是所有 OCaml 程序都可以使用的模块集。以下是列出标准库中Option 模块中包含的定义的方法

# #show Option;;
module Option :
  sig
    type 'a t = 'a option = None | Some of 'a
    val none : 'a t
    val some : 'a -> 'a t
    val value : 'a t -> default:'a -> 'a
    val get : 'a t -> 'a
    val bind : 'a t -> ('a -> 'b t) -> 'b t
    val join : 'a t t -> 'a t
    val map : ('a -> 'b) -> 'a t -> 'b t
    val fold : none:'a -> some:('b -> 'a) -> 'b t -> 'a
    val iter : ('a -> unit) -> 'a t -> unit
    val is_none : 'a t -> bool
    val is_some : 'a t -> bool
    val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
    val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
    val to_result : none:'e -> 'a t -> ('a, 'e) result
    val to_list : 'a t -> 'a list
    val to_seq : 'a t -> 'a Seq.t
  end

模块提供的定义通过在它们的名称前面添加模块名称作为前缀来引用。

# Option.map;;
- : ('a -> 'b) -> 'a option -> 'b option = <fun>

# Option.map (fun x -> x * x);;
- : int option -> int option = <fun>

# Option.map (fun x -> x * x) None;;
- : int option = None

# Option.map (fun x -> x * x) (Some 8);;
- : int option = Some 64

这里,函数Option.map 的用法在几个步骤中进行了说明。

  1. 显示它的类型。它有两个参数:类型为'a -> 'b 的函数和'a option
  2. 使用部分应用,只传递fun x -> x * x。检查结果函数的类型。
  3. None 应用。
  4. Some 8 应用。

当提供的选项值包含实际值(即,它是Some 某物)时,它会应用提供的函数并返回其结果,并将其包装在选项中。当提供的选项值不包含任何内容(即,它是None)时,结果也不包含任何内容(即,它也是None)。

本节前面使用的List.map 函数也是一个模块的一部分,即List 模块。

# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>

# List.map (fun x -> x * x);;
- : int list -> int list = <fun>

这说明了 OCaml 模块系统的第一项功能。它通过防止名称冲突来提供一种分离关注点的方法。如果两个函数由不同的模块提供,则它们即使具有不同的类型也可以具有相同的名称。

模块还允许高效的单独编译。这将在下一教程中说明。

结论

在本教程中,交互式地使用了 OCaml。下一教程您的第一个 OCaml 程序将向您展示如何编写 OCaml 文件、如何编译它们以及如何启动项目。

帮助改进我们的文档

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

OCaml

创新。社区。安全。