带标签和可选参数

先决条件

可以为函数参数指定名称和默认值。这通常被称为标签。在本教程中,我们将学习如何使用标签。

在本教程中,代码是在 UTop 中编写的。在本文件中,未加标签的参数称为位置参数

传递带标签的参数

标准库中的函数Option.value有一个名为default的参数。

# Option.value;;
- : 'a option -> default:'a -> 'a = <fun>

带标签的参数使用波浪号~传递,可以放在任何位置,并且可以按任何顺序排列。

# Option.value (Some 10) ~default:42;;
- : int = 10

# Option.value ~default:42 (Some 10);;
- : int = 10

# Option.value ~default:42 None;;
- : int = 42

注意:通过管道运算符(|>)传递带标签的参数会引发语法错误

# ~default:42 |> Option.value None;;
Error: Syntax error

标记参数

以下是如何在函数定义中命名参数的方法

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

range 的参数在函数体内部被命名为

  • lohi,就像往常一样
  • firstlast 在调用函数时;这些是标签。

以下是 range 的使用方法

# range ~first:1 ~last:10;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

# range ~last:10 ~first:1;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

当使用与标签和参数相同的名称时,可以使用更简短的语法。

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

在参数定义中,~first~first:first 相同。传递参数 ~last~last:last 相同。

传递可选参数

可选参数可以省略。当传递时,必须使用波浪号~或问号?。它们可以放在任何位置,并且可以按任何顺序排列。

# let sum ?(init=0) u = List.fold_left ( + ) init u;;
val sum : ?init:int -> int list -> int = <fun>

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

# sum [0; 1; 2; 3; 4; 5] ~init:100;;
- : int = 115

也可以将可选参数作为option类型的值传递。在传递参数时使用问号来实现这一点。

# sum [0; 1; 2; 3; 4; 5] ?init:(Some 100);;
- : int = 115

# sum [0; 1; 2; 3; 4; 5] ?init:None;;
- : int = 15

定义具有默认值的可选参数

在上一节中,我们定义了一个带有可选参数的函数,但没有解释它是如何工作的。让我们看看这个函数的不同变体

# let sum ?init:(x=0) u = List.fold_left ( + ) x u;;
val sum : ?init:int -> int list -> int = <fun>

它的行为相同,但在这种情况下,?init:(x = 0) 表示~init是一个可选参数,默认为 0。在函数内部,参数名为x

上一节中的定义使用了快捷方式,使?(init = 0)?init:(init = 0)相同。

定义没有默认值的可选参数

可以声明一个可选参数,而无需指定默认值。

# let sub ?(pos=0) ?len:len_opt s =
    let default = String.length s - pos in
    let length = Option.value ~default len_opt in
    String.sub s pos length;;
val sub : ?pos:int -> ?len:int -> string -> string = <fun>

在这里,我们定义了标准库中函数String.sub的一个变体。

  • s 是我们要从中提取子字符串的字符串。
  • pos 是子字符串的起始位置。默认为0
  • len 是子字符串的长度。如果缺少,则默认为String.length s - pos

当可选参数没有给定默认值时,它在函数内部的类型会被设置为option。在这里,len 在函数签名中显示为?len:int。但是,在函数体内部,len_opt 是一个int option

这使得以下用法成为可能

# sub ~len:5 ~pos:2 "immutability";;
- : string = "mutab"

# sub "immutability" ~pos:7 ;;
- : string = "ility"

# sub ~len:2 "immutability";;
- : string = "im"

# sub "immutability";;
- : string = "immutability"

可以为len参数和标签名称使用相同的名称。

# let sub ?(pos=0) ?len s =
    let default = String.length s - pos in
    let length = Option.value ~default len in
    String.sub s pos length;;
val sub : ?pos:int -> ?len:int -> string -> string = <fun>

可选参数和部分应用

让我们比较标准库中String.concat函数的两个可能的变体,其类型为string -> string list -> string

在第一个版本中,可选分隔符是最后声明的参数。

# let concat_warn ss ?(sep="") = String.concat sep ss;;
Line 1, characters 15-18:
  Warning 16 [unerasable-optional-argument]:
  this optional argument cannot be erased.
val concat_warn : string list -> ?sep:string -> string = <fun>

# concat_warn ~sep:"--" ["foo"; "bar"; "baz"];;
- : string = "foo--bar--baz"

# concat_warn ~sep:"";;
- : string list -> string

# concat_warn ["foo"; "bar"; "baz"];;
- : ?sep:string -> string = <fun>

在第二个版本中,可选分隔符是第一个声明的参数。

# let concat ?(sep="") ss = String.concat sep ss;;
val concat : ?sep:string -> string list -> string = <fun>

# concat ["foo"; "bar"; "baz"] ~sep:"--";;
- : string = "foo--bar--baz"

# concat ~sep:"--";;
- : string list -> string = <fun>
t
# concat ["foo"; "bar"; "baz"];;
- : string = "foobarbaz"

这两个版本之间唯一的区别是参数声明的顺序。这两个函数的行为相同,除了仅应用于参数["foo"; "bar"; "baz"]时。在这种情况下

  • concat 返回 "foobarbaz"。传递了~sep的默认值""
  • concat_warn 返回一个类型为?sep:string -> string的部分应用函数。默认值未传递。

大多数情况下,需要concat。因此,函数的最后一个声明的参数不应该可选。警告建议将concat_warn转换为concat。忽略它会公开一个具有必须提供的可选参数的函数,这是矛盾的。

注意:可选参数使得编译器难以判断函数是否被部分应用。这就是为什么在可选参数之后至少需要一个位置参数。如果在应用时存在,则表示函数被完全应用,如果缺少,则表示函数被部分应用。

使用管道操作符传递带标签的参数

将函数的无标签参数声明为第一个参数可以简化函数类型的阅读,并且不会阻止使用管道操作符传递此参数。

让我们修改之前定义的 range 函数,并添加一个额外的参数 step

# let rec range step ~first ~last = if first > last then [] else first :: range step ~first:(first + step) ~last;;
val range : int -> first:int -> last:int -> int list = <fun>

# 3 |> range ~last:10 ~first:1;;
- : int list = [1; 4; 7; 10]

仅包含可选参数的函数

当函数的所有参数都需要是可选参数时,必须添加一个虚拟的、位置的并且出现在最后一个的参数。单位 () 值对此非常有用。这就是这里所做的。

# let hello ?(who="world") () = "hello, " ^ who;;
val hello : ?who:string -> string = <fun>

# hello;;
- : ?who:string -> unit -> string = <fun>

# hello ();;
- : string = "hello, world"

# hello ~who:"sabine";;
- : unit -> string = <fun>

# hello ~who:"sabine" ();;
- : string = "hello, sabine"

# hello () ?who:None;;
- : string = "hello, world"

# hello ?who:(Some "christine") ();;
- : string = "hello, christine"

如果没有单位参数,则会发出 可选参数无法被擦除 的警告。

转发可选参数

使用问号 ? 传递可选参数允许在不展开的情况下转发它。这些示例重用了在无默认值的可选参数部分定义的 sub 函数。

# let take ?len s = sub ?len s;;
val take : ?len:int -> string -> string = <fun>

# take "immutability" ~len:2;;
- : string = "im"

# let rtake ?off s = sub ?pos:off s;;
val rtake : ?off:int -> string -> string = <fun>

# rtake "immutability" ~off:7;;
- : string = "ility"

takertake 的定义中,函数 sub 被调用,并使用带问号传递的可选参数。

take 中,可选参数与 sub 中的名称相同;写入 ?len 就足以在不展开的情况下转发。

结论

函数可以具有命名参数或可选参数。有关更多示例和标签的详细信息,请参阅参考手册

帮助改进我们的文档

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

OCaml

创新。社区。安全。