OCaml 为一些内置基本类型提供了内置打印函数: print_char, print_string, print_int, 和 print_float。还有一个 print_endline 函数,它类似于 print_string,但也输出一个换行符。
print_endline "Camels are bae"
Camels are bae
- : unit = ()
Unit
让我们来看看这些函数的类型:
print_endline
- : string -> unit = <fun>
print_string
- : string -> unit = <fun>
它们都以一个字符串作为输入,并返回一个类型为 unit 的值,这是我们以前从未见过的。这种类型只有一个值,它被写为 (),也发音为“unit”。 unit 和 bool 很像,只是 unit 类型的值比 bool 类型的值少一个。
当你需要接受一个参数或返回一个值,但没有感兴趣的值传递或返回时,就会使用 Unit。它相当于 Java 中的 void,也类似于 Python 中的 None。当你编写或使用有副作用的代码时,通常会用到 Unit。打印就是一个副作用的例子:它跟改了一切,而且无法撤销。
分号
如果你想一个接一个地打印,可以使用嵌套的 let 表达式对一些打印函数进行排序:
let _ = print_endline "Camels" in
let _ = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()
上面的 let _ = e 语法是计算 e 的一种方式,但不将其值绑定到任何名称。实际上,我们知道这些 print_endline 函数将返回的值总是 (),即 unit 值。因此,没有理由将它绑定到变量名。我们也可以写成 let () = e,表示我们知道它只是一个我们不关心的 unit 值:
let () = print_endline "Camels" in
let () = print_endline "are" in
print_endline "bae"
Camels
are
bae
- : unit = ()
但无论哪种方式,不得不写的所有 let..in 这样的样板代码都是令人讨厌的!因此,有一种特殊的语法可以用于将多个返回 unit 的函数链接在一起。 e1; e2 表达式,首先计算e1, 它应该计算为(),然后丢弃该值,并计算 e2。因此,我们可以将上述代码重写为:
print_endline "Camels";
print_endline "are";
print_endline "bae"
Camels
are
bae
- : unit = ()
这是更符合习惯的OCaml代码,而且对命令式程序员来说也更自然。
在该示例中,最后一个
print_endline后面没有分号。一个常见的错误是在每个 print 语句后面 放一个分号。相反,分号严格地放在语句之间。也就是说,分号是语句分隔符,而不是语句结束符。如果在末尾添加一个分号,上下文代码可能会出现语法错误。
Ignore
如果 e1 不是 unit 类型,那么 e1; e2 将给出警告,因为你丢弃了一个可能有用的值。如果这确实是你的目的,你可以调用内置函数 ignore : 'a -> unit 将任何值转换为 ():
(ignore 3); 5
- : int = 5
实际上,ignore 很容易实现:
let ignore x = ()
val ignore : 'a -> unit = <fun>
或者你甚至可以写下划线来指示函数接受一个值,但不将该值绑定到名称。这意味着函数永远不能在其函数体中使用该值。但是没关系:我们想要忽略它。
let ignore _ = ()
val ignore : 'a -> unit = <fun>
Printf
对于复杂的文本输出,使用内置函数进行基本类型打印很快就变得繁琐。例如,假设你想编写一个打印统计数据的函数:
(** [print_stat name num] prints [name: num]. *)
let print_stat name num =
print_string name;
print_string ": ";
print_float num;
print_newline ()
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()
我们如何缩短 print_stat?在Java中,你可以使用重载的 + 运算符将所有对象转换为字符串:
void print_stat(String name, double num) {
System.out.println(name + ": " + num);
}
但是 OCaml 的值不是对象,而且它们没有从某个根 Object 类继承的 toString() 方法。OCaml也不允许重载运算符。
然而,很久以前,FORTRAN 发明了一种不同的解决方案,其他语言,如 C 和 Java,甚至 Python 都支持这种解决方案。其思想是使用一个格式说明符来指定如何格式化输出。这个想法最广为人知的名字可能是“printf”,这是指实现它的 C 库函数的名称。许多其他语言和库仍然使用这个名称,包括 OCaml 的 Printf 模块。
下面是我们如何使用 printf 来重新实现 print_stat:
let print_stat name num =
Printf.printf "%s: %F\n%!" name num
val print_stat : string -> float -> unit = <fun>
print_stat "mean" 84.39
mean: 84.39
- : unit = ()
Printf.printf 函数的第一个参数是格式说明符。它看起来 像一个字符串,但它有更多的功能。OCaml 编译器实际上对它有很深入的理解。在格式说明符中有:
-
普通的字符,
-
以
%开头的转换说明符。
有大约 24 种转换说明符可用,你可以在 Printf 的文档 中了解这些说明符。让我们以上面的格式说明符为例。
-
它以
"%s"开头,这是字符串的转换说明符。这意味着printf的下一个参数必须是一个string,并且将输出该字符串的内容。 -
继续使用
": ",这只是普通的字符。它们被插入到输出中。 -
然后有另一个转换说明符
%F。这意味着printf的下一个参数必须具有float类型,并且将以与 OCaml 打印浮点数相同的格式输出。 -
在它之后的换行符
"\n"是另一个普通字符序列。 -
最后,转换说明符
"%!"意味着刷新输出缓冲区。正如你在早期编程课程中所了解的那样,输出通常是缓冲的,这意味着输出不会立即发生。刷新缓冲区可以确保缓冲区中仍然存在的任何内容立即获得输出。这个说明符的特殊之处在于,它实际上不需要printf的另一个参数。
如果参数的类型相对于转换说明符不正确,OCaml 将检测到这一点。让我们添加一个类型注解,强制 num 为 int 类型,然后看看浮点数转换说明符 %F 会发生什么:
let print_stat name (num : int) =
Printf.printf "%s: %F\n%!" name num
File "[14]", line 2, characters 34-37:
2 | Printf.printf "%s: %F\n%!" name num
^^^
Error: This expression has type int but an expression was expected of type
float
错误:该表达式的类型是 int,但是该表达式需要一个 float 类型
要解决这个问题,可以把 int 的转换说明符改成 %i :
let print_stat name num =
Printf.printf "%s: %i\n%!" name num
val print_stat : string -> int -> unit = <fun>
printf 的另一个非常有用的变体是 sprintf,它以字符串的形式采集输出,而不是打印出来:
let string_of_stat name num =
Printf.sprintf "%s: %F" name num
val string_of_stat : string -> float -> string = <fun>
string_of_stat "mean" 84.39
- : string = "mean: 84.39"
注:本书是康奈尔大学 CS 3110 数据结构和函数式编程的教材。原书为英文版,在学习的过程中,根据自己的理解,翻译了一些,做一个记录,版权归原作者所有,如有侵权,请联系我删除。