带标签和可选参数
先决条件
可以为函数参数指定名称和默认值。这通常被称为标签。在本教程中,我们将学习如何使用标签。
在本教程中,代码是在 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
的参数在函数体内部被命名为
lo
和hi
,就像往常一样first
和last
在调用函数时;这些是标签。
以下是 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"
在 take
和 rtake
的定义中,函数 sub
被调用,并使用带问号传递的可选参数。
在 take
中,可选参数与 sub
中的名称相同;写入 ?len
就足以在不展开的情况下转发。
结论
函数可以具有命名参数或可选参数。有关更多示例和标签的详细信息,请参阅参考手册。