具名與可選參數

先決條件

可以為函式參數賦予名稱和預設值。這廣泛稱為標籤。在本教程中,我們學習如何使用標籤。

在本教程中,程式碼以 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 -> string list -> stringString.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"

takertake 的定義中,函數 sub 被呼叫時,會使用問號傳遞可選參數。

take 中,可選參數的名稱與 sub 中的名稱相同;寫 ?len 足以在不解包的情況下轉發。

結論

函數可以有具名或可選參數。請參閱參考手冊,以了解更多關於標籤的範例和詳細資訊。

仍然需要協助嗎?

協助改進我們的文件

所有 OCaml 文件都是開源的。發現錯誤或不清楚的地方嗎?提交一個 pull request。

OCaml

創新。社群。安全。