高階函式

簡介

在 OCaml 中,使用函式很快就會成為第二天性。我們喜歡認為函式描述了世界,就像那樣,我們通常會編寫描述事物行為方式的函式。

以一個向某人打招呼的函式為例

# let say_hi name = print_string ("Hello, " ^ name ^ "!\n") ;;
val say_hi : string -> unit = <fun>

我們可以多次呼叫此函式,向多人說「hello」

#say_hi "Xavier";;
Hello, Xavier!
- : unit = ()

# say_hi "Sabine";;
Hello, Sabine!
- : unit = ()

# say_hi "Joe";;
Hello, Joe!
- : unit = ()

如果我們想多次向同一個人說「hello」,我們只需要重複相同的程式碼行。

# say_hi "Camel";;
Hello, Camel!
- : unit = ()

# say_hi "Camel";;
Hello, Camel!
- : unit = ()

# say_hi "Camel";;
Hello, Camel!
- : unit = ()

我們可以避免每次都重複這些程式碼行的一種方法是編寫一個函式來重複說「hi」3 次

# let say_hi_3_times name =
  say_hi name;
  say_hi name;
  say_hi name
;;
val say_hi_3_times : string -> unit = <fun>

在這個函式中,我們可以看到一些行為

  • 它向同一個名字說「hi」。
  • 它重複了正好 3 次。

但是,如果我們想說「hi」2 次、4 次或 12 次,會發生什麼?

當這種情況發生時,通常表示函式正在做出某些不應該做的決定。換句話說,函式知道一些事情(例如次數)。

因此,我們將建立一個函式,讓呼叫者決定要說「hi」多少次。我們透過要求一個新的參數來做到這一點,在本例中為 times

# let rec say_many_hi times name =
  if times < 1 then ()
  else begin
    say_hi name;
    say_many_hi (times - 1) name
  end
;;
val say_many_hi : int -> string -> unit = <fun>

好多了。現在我們可以呼叫

# say_many_hi 3 "Xavier";;
Hello, Xavier!
Hello, Xavier!
Hello, Xavier!
- : unit = ()

# say_many_hi 12 "Camel";;
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
Hello, Camel!
- : unit = ()

不幸的是,重複使用這種重複行為並不容易,因為我們已經硬式編碼了對 say_hi 的呼叫。

為了使其可重複使用,我們可以讓呼叫者決定函式應該做什麼

# let rec repeat times thing_to_do =
  if times < 1 then ()
  else begin
    thing_to_do;
    repeat (times - 1) thing_to_do
  end
;;
val repeat : int -> 'a -> unit = <fun>

但是 thing_to_do 應該是什麼?我們的直覺可能是我們可以呼叫

# repeat 3 (say_hi "Camel");;
Hello, Camel!
- : unit = ()

但是我們的程式只輸出一個問候語

Hello, Camel!

這不是我們想要的!我們希望它向 Camel 說「hello」3 次。

在 OCaml 中,參數會在函式本身之前進行評估,因此在本例中,我們在到達重複函式之前就說了「hi」。

我們可以讓 repeat 多次呼叫 say_hi,方法是透過另一個函式包裝它來延遲函式的執行,就像這樣

# repeat 3 (
fun () ->
    say_hi "Camel");;

這表示我們必須重構 repeat 函式,方法是將 thing_to_do 取代為 thing_to_do (),以呼叫我們的新函式

let rec repeat times thing_to_do =
  if times < 1 then ()
  else (
    thing_to_do ();
    repeat (times - 1) thing_to_do)
;;

thing_to_do 重新命名為 fn 後,我們得到了一個很棒的 repeat 函式

# let rec repeat times fn =
  if times < 1 then ()
  else begin
    fn ();
    repeat (times - 1) fn
  end
;;
val repeat : int -> (unit -> 'a) -> unit = <fun>

我們可以使用 repeat 來重新建立我們原始的 say_many_hi,或重複任意次數的任何工作

# let say_many_hi times name = repeat times (fun () -> say_hi name);;
val say_many_hi : int -> string -> unit = <fun>
  
# let print_big_space () = repeat 10 print_newline;;
val print_big_space : unit -> unit = <fun>

這就是高階函式的力量。它們讓您能夠從更簡單的函式建立複雜的行為。

以下是一些來自真實世界的其他範例

# let say_hi_to_many names = List.iter say_hi names;;
val say_hi_to_many : string list -> unit = <fun>


# module StringSet = Set.Make(String);;
module StringSet :
  sig
    type elt = string
    type t = StringSet.t
    val empty : t [...]
  end

# let only_once fn names =
  names
  |> StringSet.of_list
  |> StringSet.iter fn;;
val only_once : (string -> unit) -> string list -> unit = <fun>
 
# let yell_hi name =
  name
  |> String.uppercase_ascii
  |> say_hi;;
val yell_hi : string -> unit = <fun>
  
# let call_for_dinner names = only_once yell_hi names;;
val call_for_dinner : string list -> unit = <fun>

常見高階函式

在實際應用中,有些模式會一遍又一遍地重複。熟悉它們會很有用,因為它們是函數式程式設計師的常用詞彙。其中一些是

  • 柯里化與反柯里化
  • 管道化、組合與鏈結
  • 迭代
  • 篩選
  • 映射
  • 摺疊(或縮減)
  • 排序
  • 綁定(或扁平映射)

柯里化與反柯里化

由於在 OCaml 中,所有函式實際上只接受一個參數,因此當您呼叫 add x y 時,您實際上是在呼叫兩個函式!((add x) y)

有時,按不同的順序應用函式的部分會有幫助,有時,讓函式真正地一次接受其所有參數會有幫助。

這就是我們所謂的柯里化與反柯里化

  • 柯里化的 add 函式會像 add x y 這樣被呼叫。
  • 反柯里化的 add 函式會像 add (x, y) 這樣被呼叫。請注意,這實際上只有一個參數!

在我們開始看一些範例之前,讓我們先定義一些輔助函式,這將幫助我們柯里化與反柯里化函式。

反柯里化

我們的反柯里化輔助函式是一個將一個函式作為輸入並傳回另一個函式的函式。它本質上是一個包裝器。

輸入函式的類型必須為 'a -> 'b -> 'c。這是任何接受 2 個參數的函式的類型。

輸出函式的類型將為 ('a * 'b) -> 'c。請注意,參數 'a'b 現在如何捆綁在一個元組中!

這是我們的輔助函式

(* [uncurry] takes a function that is normally curried,
   and returns a function that takes all arguments at once. *)
# let uncurry f (x, y) = f x y;;
val uncurry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun>

如果我們想為更多參數寫 uncurry,我們只需建立一個新的 uncurry3uncurry4 甚至是 uncurry5 函式,它們的作用將完全相同

# let uncurry4 f (w, x, y, z) = f w x y z;;
val uncurry4 : ('a -> 'b -> 'c -> 'd -> 'e) -> 'a * 'b * 'c * 'd -> 'e = <fun>

當您處理列表(我們在 OCaml 中經常這樣做)且列表碰巧具有元組時,反柯里化會非常有用。

以這個名稱和最喜歡的表情符號的元組列表為例

# let people = [
  "🐫", "Sabine";
  "🚀", "Xavier";
  "", "Louis";
]
;;

如果我們想對其中任何一個元素執行某些操作,我們需要分割元組並呼叫函式

# let greet emoji name =
  Printf.printf "Glad to see you like %s, %s!\n" emoji name
;;

let emoji, name = List.hd people in
# greet emoji name
;;

但是,我們也可以反柯里化我們的 greet 函式來對整個元組進行操作!

# uncurry greet (List.hd people)
;;

柯里化

另一方面,有時我們有已經使用元組的函式,我們想像使用接受多個參數的函式一樣使用它們。

為此,我們可以定義一個小的 curry 輔助函式,它將一個函式作為輸入,並傳回另一個函式作為輸出。它本質上是一個包裝器。

輸入函式的類型必須為:('a * 'b) -> 'c – 這是任何使用具有 2 個參數的元組的函式的類型。

輸出函式的類型將會是 'a -> 'b -> 'c – 注意到引數 'a'b 現在已經被解綁了!

這是我們的輔助函式

(* [curry] takes a function that is normally curried,
   and returns a function that takes all arguments at once. *)
let curry f x y = f (x, y)
;;

如果我們想要為更多引數編寫 curry 函式,我們只需建立一個新的 curry3curry4 甚至是 curry5 函式,它們的工作方式完全相同

let curry4 f w x y z = f (w, x, y, z)
;;

當你處理列表時(在 OCaml 中我們經常這樣做),以及當列表只有單一值,但你的函式需要多個引數時,柯里化會非常有用。

例如,這個名稱列表和一個未柯里化的顯示函式

let names = [
  "Sabine";
  "Xavier";
  "Louis";
]
;;

let reveal (title, name) =
  Printf.printf "But it was %s, %s!\n" title name
;;

如果我們想要在名稱上使用 reveal,我們必須將其放入一個元組,然後進行呼叫。像這樣

List.iter (fun name ->
  let title = "The OCamler" in
  reveal (title, name)) names
;;

但我們也可以柯里化我們的 reveal 函式,使其接受 2 個引數!

List.iter (curry reveal "The OCamler") names
;;

可讀性注意事項

在你的程式碼變得難以閱讀之前,柯里化和反柯里化都只是好玩的遊戲。

有時柯里化一個函式是有道理的,有時更清晰的做法是手動將其包裝在函式中。

例如,這是一個包含大量柯里化/反柯里化的管道,如果我們手動寫出包裝函式,可能會更容易閱讀和維護

let do_and_return f x = f x; x
;;

let flip (x, y) = (y, x)
;;

names 
|> List.map (do_and_return (greet "👋🏼"))
|> List.map (Fun.flip List.assoc (List.map flip people))
|> List.iter (curry reveal "The OCamler")
;;

相較之下,這是一個更易讀且更容易維護的版本

let find_by_name name1 =
  List.find (fun (_emoji, name2) -> name1 = name2) people
;;

(* first iterate over the names, greeting them *)
names |> List.iter (greet "👋🏼")
;;

(* then find the right emoji by name *)
names
|> List.map find_by_name
|> List.iter (fun (emoji, _name) -> reveal ("The OCamler", emoji))
;;

管道、組合和鏈接

在 OCaml 中,我們經常使用函式,所以值會從一個函式傳遞到另一個函式,形成我們喜歡稱之為管道的東西。

let a = foo () in
let b = bar a in
let c = baz b in
(* ... *)

當然,我們總是可以用巢狀的方式呼叫函式,以避免額外的變數和所有這些輸入

let c = baz (bar (foo ())) in
(* ... *)

但這有時不太容易閱讀,尤其是當函式數量增加時,因為它是從內到外的。

為了避免這種情況,我們可以使用 |> 運算子

let c = foo () |> bar |> baz in 
(* ... *)

這個運算子會轉換為我們手動執行過的完全相同的巢狀呼叫,實際上沒有什麼神奇之處。它被定義為一個函式

(* the pipeline operator *)
let (|>) x fn = fn x

它接收一個值 x 和一個函式 fn,一旦它同時擁有兩者,它就會用 x 呼叫 fn。這讓我們可以反轉順序,並建立從左到右或從上到下讀取的管道,而不是從內到外讀取。

但是當我們的函式有多個引數時會發生什麼?

讓我們看一個字串操作的例子。我們想要從電子郵件中取得網域名稱。

let email = "ocaml.mycamel@ocaml.org"
;;

email
|> String.split_on_char '@'
|> Fun.flip List.nth 0
|> Option.map (fun str -> String.sub str 0 5)
|> Option.get
;;

由於 OCaml 預設會柯里化函式,因此只用部分引數部分應用函式,並將最後一個引數留在管道中傳遞是很實用的。

對於最後一個位置具有最重要引數的函式(我們稱之為 t-last),以及使用標籤引數並允許將最重要的引數最後傳遞(通過先傳遞所有名稱引數)的函式(我們通常稱之為 t-first),這都是正確的。

這兩種情況聽起來非常相似,但在可用性方面有很大的實際差異。讓我們重新審視上面的例子,使用這些函式的標籤引數版本

open StdLabels

module List = struct
  include List
  let nth_opt t ~at = nth_opt at t
end

email
|> String.split_on_char ~sep:'@'
|> List.nth_opt ~at:0
|> Option.map (String.sub ~off:0 ~len:5)
|> Option.value ~default:"new-user"
;;

(** 注意(@leostera):這個例子有點糟糕,我想要一個使用標籤可以大幅改善可讀性的例子,但由於 ListLabels.nth_opt 不接受引數,我們仍然需要那個糟糕的 fun flip :( 會回頭處理這個)

迭代

當我們想到迴圈以及遍歷事物集合時,我們通常會想到迭代

  • 迴圈遍歷列表中的元素
  • 遍歷映射中的鍵

但在 OCaml 中,迭代的模式可以擴展到其他資料類型,例如可選值或結果、或樹和惰性序列。

在 OCaml 中迭代意味著,如果存在一個(或多個)值,我們希望將一個函式應用於它。

遍歷列表

OCaml 中的列表是一個連結列表,它由頭(第一個元素)和尾(列表的其餘部分)組成。

我們可以通過對它們進行模式比對來遍歷列表。這樣做時,我們要麼得到一個空列表([]),要麼得到一個具有頭和尾的模式(n :: rest)。在具有頭和尾的分支上,我們可以直接使用頭值並將函式應用於它,然後遞迴處理尾。

let rec print_nums nums =
  match nums with
  (* if the list is empty, we do nothing *)
  | [] -> ()
  (* if the list is not empty... *)
  | n :: rest ->
    (* we print the first element *)
    Printf.printf "%d\n" n;
    (* and repeat over the rest of the list *)
    print_nums rest

現在,如果我們想對列表的每個元素執行其他操作,我們只需要求一個將在它們上執行的函式

let rec print_all fn nums =
  match nums with
  | [] -> ()
  | n :: rest ->
    fn n;
    print_all fn rest

這樣,我們可以使用任何函式 fn 呼叫 print_all,所以它實際上只是遍歷列表並在每個元素上執行函式。

這正是標準程式庫中 List.iter 的定義方式。

遍歷可選值和結果

在 OCaml 中,另一種常見的迭代資料類型是可選值和結果值。通常,我們只希望在選項中具有某個值或存在Ok 值時才執行函式。如果其中沒有值,或者我們有一個錯誤,我們就不想執行任何操作。

我們可以通過對值進行模式比對,並在 Some 或 Ok 分支中對內部值呼叫我們的函式來做到這一點。

let run_if_some opt fn =
  match opt with
  | Some value -> fn value
  | None -> ()
;;
 
let run_if_ok res fn =
  match res with
  | Ok value -> fn value
  | Error _  -> ()
;;

這就是標準程式庫中 Option.iterResult.iter 的定義方式。

遍歷映射和集合

更大的資料集合(如映射和集合)在 OCaml 中也很常見。我們為它們提供了專用的模組,但它們具有函子介面。這表示你無法真正直接使用 SetMap,而必須呼叫模組層級函式 Set.Make 來為要儲存在其中的特定類型建立自訂版本的 Set 模組。

建立 Set 或 Map 模組後,你會發現它們提供了將其值轉換為列表的函式。

使用這兩個函式中的任何一個,我們可以將迭代器放在映射或集合上

let iter values collection fn  =
  let values : 'a list = values collection in 
  List.iter fn values
;;
  
module StringSet = Set.Make(String);;
module IntMap = Map.Make(Int);;

let iter_map map fn = iter IntMap.bindings map fn ;;
let iter_set set fn = iter StringSet.elements set fn ;;

你會注意到,這次我們沒有使用模式比對來直接遍歷 Map 或 Set 的值。這是因為 Set 和 Map 的表示是私有的。

映射和集合的迭代函式的實際實作在底層確實使用了模式比對。

遍歷惰性序列

通常,實作某些資料類型的模組將提供遍歷它的函式。

但有些資料是惰性的,它只允許我們一次存取一個元素。因此,如果資料看起來像 [1,2,3],你只有在存取第一個和第二個值之後才能計算第三個值。

OCaml 中的惰性序列以 Seq 模組表示,該模組具有一個名為 uncons 的函式來取得下一個元素。此函式還會傳回我們可以用來取得第二個元素的新序列,依此類推。

let rec iter seq fn = 
  match Seq.uncons seq with
  | None -> ()
  | Some (value, seq2) -> 
    fn value;
    iter seq2 fn
;;

此函式將嘗試取得序列的第一個元素,如果有,則對其執行函式 fn。然後,它將在新序列 (seq2) 上重複,該序列從第二個元素開始。

這幾乎與標準程式庫中 Seq.iter 的定義方式完全相同。

遍歷自訂資料類型

到目前為止,我們已經了解了如何遍歷標準程式庫中的資料類型。現在,我們將了解如何遍歷我們自己的樹資料類型。

我們將定義我們的樹類型,使其包含 2 個建構子。一個用於葉節點,它是樹末端的節點。另一個用於有子系的節點。

type 'value tree =
 | Leaf of 'value 
 | Node of 'value tree * 'value
;;

因此,我們的資料類型允許我們表示樹狀資料

graph TD
  Node1([Node]) --> ML
  Node1 --> Node2
  Node2([Node]) --> Caml((Caml))
  Node2 --> Node3
  Node3([Node]) --> CamlLight([CamlLight])
  Node3 --> Leaf
  Leaf  --> OCaml

現在,在我們定義迭代函式之前,重要的是要定義迭代對我們的資料類型意味著什麼。我們想要從樹的底部開始迭代嗎?我們想要從頂部開始迭代嗎?我們應該由中間向外迭代嗎?

在我們的範例中,我們將從上到下進行迭代

let rec iter tree fn = 
  match tree with
  | Leaf value -> fn value
  | Node (tree2, value) -> 
      fn value;
      iter tree2 fn
;;

同樣,我們通過對值進行模式比對、將函式應用於解構的值,並遞迴處理剩餘的資料來迭代。

映射

與迭代相反,有時我們希望對某些資料應用函式,並且我們希望在不改變資料形狀的情況下保留結果。

例如,如果我們有一個使用者列表,我們可能想要取得一個使用者名稱列表。或者,如果我們有一個可選的密碼,我們可能只想在設定密碼時才對其進行加密。

這稱為映射

映射列表

映射列表與遍歷列表非常相似。我們對列表進行模式比對、取得它的頭、對其執行函式,並遞迴處理主體。

主要區別在於,我們不會丟棄對元素執行函式所產生的結果值,而是會從它重建列表。

let rec map list fn =
  match list with
  | [] -> []
  | head :: tail -> (fn head) :: (map tail fn)
;;

請注意,我們如何使用 :: 建構子來解構列表和重建列表。

映射選項

只有當我們想要在選項內有值時變更內容時,映射可選值才有意義。也就是說,如果我們有一個 None,則沒有任何東西要映射,因此我們只能映射 Some x 值。

let map opt fn =
  match opt with
  | Some value -> Some (fn value)
  | None -> None
;;

請注意,比對的兩側都傳回相同的東西:如果我們有一個 None,我們就傳回 None;如果我們有一個 Some,我們就傳回一個 Some。這樣,結構就會被保留。

映射結果

當我們有一個結果時,映射會變得有點棘手。我們現在有 2 種可能的方式可以變更內部值,它們都是完全有效的映射!

我們可以映射 Ok 值建構子中的值,或者我們可以映射 錯誤原因建構子中的錯誤值。

(* maps a result over the ok value *)
let map_ok res fn =
  match res with
  | Ok value -> Ok (fn value)
  | Error reason -> Error reason
;;

(* maps a result over the error value *)
let map_err res fn =
  match res with
  | Ok value -> Ok value
  | Error reason -> Error (fn reason)
;;

這兩者在不同的情況下都很有用,例如想要變更錯誤類型,或僅在我們有 Ok 值時才執行操作。

映射自訂資料類型

當使用我們的自訂資料型別時,例如在「迭代」章節中使用的 tree,我們應該盡可能保留資料的結構。也就是說,如果我們對其進行映射(map),我們會期望相同的節點和節點之間的連接,但其中的值會不同。

let rec map tree fn =
  match tree with
  | Leaf value -> Leaf (fn value)
  | Node (tree2, value) -> Node (map tree2 fn, fn value)
;;

請注意,樹的結構被保留了,但每次遇到 value 時,我們都會用 (fn value) 更新它。

摺疊(或歸約)

有時我們希望迭代資料並在從一個元素移動到另一個元素的過程中收集結果。這種行為稱為「摺疊」或「歸約」,它對於總結資料非常有用。

例如,如果我們有一個從 1 到 10 的數字列表,我們可以通過摺疊將它們加總。

let sum = List.fold_left (+) 0 [1;2;3;4;5;6;7;8;9;10];;

如果我們想對「迭代」章節中看到的自訂樹狀型別實作總和,我們可以這樣做:

type 'value tree =
 | Leaf of 'value 
 | Node of 'value tree * 'value
;;

let rec sum_tree tree =
  match tree with
  | Leaf value -> value
  | Node (tree2, value) -> value + (sum_tree tree2)
;;

如果我們將其推廣為對樹應用任何轉換並將其歸約為單一值,我們需要將 + 函數替換為參數 fn

let rec fold_tree tree fn =
  match tree with
  | Leaf value -> fn value
  | Node (tree2, value) -> fn value (fold_tree tree2 fn)
;;

但我們很快就會遇到一個問題:我們的 fn 函數旨在組合兩個項目,因此在 Leaf 分支中,第二個項目是什麼?

摺疊要求我們定義一個*零值*,這是累加器的起點,當集合或資料型別「空」時將使用它。

有些資料型別沒有好的「空」值。例如,我們的樹就沒有。列表有一個空列表。選項有一個 None 建構子。結果(Result)也沒有好的「空」值。

因此,要修正我們的 fold_tree,我們只需要傳入一個零或累加器值:

let rec fold_tree tree fn acc =
  match tree with
  | Leaf value -> fn value acc
  | Node (tree2, value) -> fn value (fold_tree tree2 fn acc)
;;

瞧!我們的函數現在類型正確,我們可以將它用於將我們的樹歸約為任何值。

排序

另一個通常使用高階函數實現的常見行為是對集合進行排序。

Array.sortList.sort 都實作了一個介面,如果您傳入一個知道如何比較 2 個元素的函數,它們將調整排序以使用此比較。

對於陣列,此操作會就地修改陣列。

let array = [| 4;9;1;10 |];;

(* sorts the array in ascending order *)
Array.sort (fun a b -> a - b) array;;

(* sorts the array in descending order *)
Array.sort (fun a b -> b - a) array;;

對於列表,此操作會返回一個新的排序列表。

let list = [4;9;1;10] ;;
let asc = List.sort (fun a b -> a - b) list ;;
let desc = List.sort (fun a b -> b - a) list ;;

大多數 OCaml 模組都包含一個可以傳遞給 sortcompare 函數。

let int_array = [|3;0;100|];;
Array.sort Int.compare int_array;;

List.sort String.compare ["z";"b";"a"];;

List.sort Bool.compare [true;false;false];;

綁定

函數式程式設計中最後一個常見的高階模式是能夠*從內部聯接*資料的能力。由於歷史原因,這通常稱為*綁定(bind)*。

例如,如果我們有一個列表,並使用返回列表的函數對其進行映射,那麼我們將有一個列表的列表。有時我們需要這樣,但有時我們更希望新列表是*展平*而不是*巢狀*。

為了對列表執行此操作,我們可以使用 concat_map 函數,該函數如下所示:

let twice x = [x;x];;
let double ls = List.concat_map twice ls;;

double [1;2;3];; (* [1;1;2;2;3;3] *)

綁定作為提早返回

這種相同的模式可用於建立在特定值上*短路*的函數鏈。

例如,如果您必須從資料庫中檢索使用者,並且*只有在有使用者時*才嘗試存取使用者的電子郵件,您可以使用 Option.bind 在第一個操作上短路。

type user = {
    email: string option
}

let get_user () = None ;;
let get_email user = user.email ;;

let email = Option.bind (get_user ()) get_email ;;

在這個例子中,由於我們的 get_user () 呼叫返回 None,因此永遠不會呼叫 get_email。只有當 get_user 返回 Some user 時,才會呼叫 get_email

這也適用於結果值:

type user = {
    email: string
}

let get_user () = Error `no_database ;;
let get_email user = Ok user.email ;;

let email = Result.bind (get_user ()) get_email ;;

主要區別在於,在這種情況下,Result.bind 偏向於 Ok value,因此如果結果值是 Error reason,它將通過綁定短路,並僅返回出現的第一個錯誤。

Let 運算子

不幸的是,呼叫 Result.bind 可能有點笨拙。我們無法像呼叫 map 那樣通過一系列綁定傳遞值。例如,這是無效的:

let email = get_user ()
           |> Result.bind get_email
           |> Result.bind extract_domain
           |> Result.bind validate_domain
           ;;

值得慶幸的是,OCaml 允許我們重新定義一組稱為 *let 運算子* 的運算子子集,這些運算子可用於簡化 bind 呼叫的使用,使其看起來非常接近正常的 let 綁定:

(* first we will declare out let* operator to be equal to Result.bind *)
let (let*) = Result.bind

let* user = get_user () in
let* email = get_email user in
let* domain = extract_domain email in
validate_domain domain

這樣做的好處是使程式碼更具可讀性,而不會改變我們期望從 bind 呼叫中獲得的行為。

非同步程式碼

用於 OCaml 的非同步函式庫,實作 Promise/Futures 通常也有一個 bind 函數,允許您串聯計算。

仍然需要協助嗎?

協助改進我們的文件

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

OCaml

創新。社群。安全性。