具名與可選參數
先決條件
可以為函式參數賦予名稱和預設值。這廣泛稱為標籤。在本教程中,我們學習如何使用標籤。
在本教程中,程式碼以 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 -> string list -> string
的 String.concat
函式的兩種可能變體。
在第一個版本中,可選的分隔符號是最後宣告的參數。
# 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
足以在不解包的情況下轉發。
結論
函數可以有具名或可選參數。請參閱參考手冊,以了解更多關於標籤的範例和詳細資訊。