值與函式
簡介
在 OCaml 中,函式被視為值,因此您可以將函式當作函式的引數,並從函式中回傳它們。本教學介紹了表達式、值和名稱之間的關係。前四個章節處理非函式值。以下章節,從 函式即值 開始,處理函式。
我們使用 UTop 以範例來理解這些概念。建議您修改範例以獲得更好的理解。
什麼是值?
像大多數函數式程式語言一樣,OCaml 是一種以表達式為導向的程式語言。這意味著程式是表達式。事實上,幾乎所有東西都是表達式。在 OCaml 中,陳述式不指定要對資料執行的動作。所有計算都是透過表達式求值來完成的。計算表達式會產生值。以下,您會找到一些表達式、其型別以及產生的值的範例。有些包含計算,有些則不包含
# "Every expression has a type";;
- : string = "Every expression has a type"
# 2 * 21;;
- : int = 42
# int_of_float;;
- : float -> int = <fun>
# int_of_float (3.14159 *. 2.0);;
- : int = 6
# fun x -> x * x;;
- : int -> int = <fun>
# print_endline;;
- : string -> unit = <fun>
# print_endline "Hello!";;
Hello!
- : unit
表達式的型別(在求值之前)及其產生值的型別(在計算之後)是相同的。這讓編譯器可以避免在二進位檔中進行執行期型別檢查。在 OCaml 中,編譯器會移除型別資訊,因此它在執行期不可用。在程式理論中,這稱為主語歸約。
全域定義
每個值都可以命名。這就是 let … = …
陳述式的用途。名稱在左側;表達式在右側。
- 如果可以求出表達式的值,就會求值。
- 否則,表達式會按原樣轉換為值。這是函式定義的情況。
這是在 UTop 中撰寫定義時發生的情況
# let the_answer = 2 * 3 * 7;;
val the_answer : int = 42
全域定義是那些在最上層輸入的定義。這裡,the_answer
是全域定義的。
區域定義
區域定義會在表達式內繫結一個名稱
# let d = 2 * 3 in d * 7;;
- : int = 42
# d;;
Error: Unbound value d
區域定義是由 let … = … in …
表達式引入的。在 in
關鍵字之前繫結的名稱僅在 in
關鍵字之後的表達式中繫結。在這裡,名稱 d
在表達式 d * 7
內繫結到 6
。
幾個注意事項
- 本範例中沒有引入全域定義,這就是為什麼我們會收到錯誤。
2 * 3
的計算將始終在d * 7
之前發生。
區域定義可以鏈結(一個接一個)或巢狀(一個在另一個內)。以下是鏈結的範例
# let d = 2 * 3 in
let e = d * 7 in
d * e;;
- : int = 252
# d;;
Error: Unbound value d
# e;;
Error: Unbound value e
以下是範圍運作的方式
d
在let e = d * 7 in d * e
內繫結到6
e
在d * e
內繫結到42
以下是巢狀的範例
# let d =
let e = 2 * 3 in
e * 5 in
d * 7;;
- : int = 210
# d;;
Error: Unbound value d
# e;;
Error: Unbound value e
以下是範圍運作的方式
e
在e * 5
內繫結到6
d
在d * 7
內繫結到30
允許任意組合的鏈結或巢狀結構。
在這兩個範例中,d
和 e
都是區域定義。
定義中的模式比對
當模式比對只有一個情況時,它可以用於名稱定義以及 let ... =
和 fun ... ->
表達式中。在這種情況下,可以定義少於或多於一個的名稱。這適用於元組、記錄和自訂單變體型別。
元組上的模式比對
常見的情況是元組。它允許使用單個 let
建立兩個名稱。
# List.split;;
- : ('a * 'b) list -> 'a list * 'b list
# let (x, y) = List.split [(1, 2); (3, 4); (5, 6); (7, 8)];;
val x : int list = [1; 3; 5; 7]
val y : int list = [2; 4; 6; 8]
List.split
函式將成對的列表轉換為列表對。在這裡,每個產生的列表都繫結到一個名稱。
記錄上的模式比對
我們可以對記錄進行模式比對
# type name = { first : string; last: string };;
type name = { first : string; last : string; }
# let robin = { first = "Robin"; last = "Milner" };;
val robin : name = {first = "Robin"; last = "Milner"}
# let { first = given_name; last = family_name } = robin;;
val given_name : string = "Robin"
val family_name : string = "Milner"
函式參數中的模式比對
單一情況模式比對也可以用於參數宣告。
以下是元組的範例
# let get_country ((country, { first; last }) : string * name) = country;;
val get_country : string * name -> string = <fun>
以下是 name
記錄的範例
# let introduce {first; last} = "I am " ^ first ^ " " ^ last;;
val introduce : name -> string = <fun>
注意 也可能對參數宣告使用 discard
模式。
# let get_meaning _ = 42;;
val get_meaning : 'a -> int = <fun>
unit
上的模式比對
組合定義和模式比對的特殊情況涉及 unit
型別
# let () = print_endline "ha ha";;
ha ha
注意:如同 OCaml 導覽教學中解釋的,unit
型別只有一個值 ()
,發音為「unit」。
在上方,模式不包含任何識別符號,表示沒有定義任何名稱。該表達式會被求值,並產生副作用(將 ha ha
印到標準輸出)。
注意:為了讓編譯後的檔案僅為其副作用評估表達式,您必須將它們寫在 let () =
之後。
使用者定義型別的模式比對
這也適用於使用者定義的型別。
# type citizen = string * name;;
type citizen = string * name
# let ((country, { first = forename; last = surname }) : citizen) = ("United Kingdom", robin);;
val country : string = "United Kingdom"
val forename : string = "Robin"
val surname : string = "Milner"
使用模式比對捨棄值
如最後一個範例所示,萬用字元模式 (_
) 可以用於定義中。
# let (_, y) = List.split [(1, 2); (3, 4); (5, 6); (7, 8)];;
val y : int list = [2; 4; 6; 8]
List.split
函式會回傳一個列表的配對。我們只對第二個列表感興趣,我們將其命名為 y
,並使用 _
捨棄第一個列表。
作用域和環境
在不將其過度簡化的情況下,OCaml 程式是一系列的表達式或全域 let
定義。
執行會從上到下評估每個項目。
在評估期間的任何時間,環境是可用定義的已排序序列。環境在其他語言中也稱為上下文。
在這裡,名稱 twenty
會被添加到最上層的環境中。
# let twenty = 20;;
val twenty : int = 20
twenty
的作用域是全域的。此名稱在定義之後的任何地方都可用。
在這裡,全域環境沒有改變
# let ten = 10 in 2 * ten;;
- : int = 20
# ten;;
Error: Unbound value ten
評估 ten
會導致錯誤,因為它尚未被添加到全域環境中。然而,在表達式 2 * ten
中,區域環境包含 ten
的定義。
儘管 OCaml 是一種以表達式為導向的語言,但它有一些陳述。全域 let
會透過添加名稱-值綁定來修改全域環境。
最上層的表達式也是陳述,因為它們等同於 let _ =
定義。
# (1.0 +. sqrt 5.0) /. 2.0;;
- : float = 1.6180339887498949
# let _ = (1.0 +. sqrt 5.0) /. 2.0;;
- : float = 1.6180339887498949
內部遮蔽
一旦您建立名稱、定義它並將其繫結到值,它就不會改變。也就是說,可以再次定義名稱以建立新的繫結。
# let i = 21;;
val i : int = 21
# let i = 7 in i * 2;;
- : int = 14
# i;;
- : int = 21
第二個定義會遮蔽第一個定義。內部遮蔽僅限於區域定義的作用域。因此,之後寫入的任何內容仍然會採用先前的定義,如上所示。在這裡,i
的值沒有改變。它仍然是 21
,如同在第一個表達式中所定義的。第二個表達式會在區域範圍內,於 i * 2
內部繫結 i
,而不是全域範圍。
同層級遮蔽
當在同一個層級有兩個相同名稱的定義時,會發生另一種遮蔽。
# let h = 2 * 3;;
val h : int = 6
# let e = h * 7;;
val e : int = 42
# let h = 7;;
val h : int = 7
# e;;
- : int = 42
現在環境中有兩個 h
的定義。第一個 h
沒有改變。當定義第二個 h
時,第一個 h
就變得無法訪問。
函數作為值
在 OCaml 中,函數是值。這是函數式程式設計的關鍵概念。在此情境中,也可以說 OCaml 具有一級函數。
應用函數
當並排寫入多個表達式時,最左邊的表達式應該是一個函數。所有其他的都是引數。在 OCaml 中,不需要括號來表達將引數傳遞給函數。括號只有一個用途:將表達式關聯以建立子表達式。
# max (21 * 2) (int_of_string "713");;
- : int = 713
max
函數會回傳兩個引數中較大的值,它們是
42
,21 * 2
的結果713
,int_of_string "713"
的結果
在建立子表達式時,也可以使用 begin ... end
。這與使用括號 ( ... )
相同。因此,以上內容也可以重寫並獲得相同的結果
# max begin 21 * 2 end begin int_of_string "713" end;;
- : int = 713
# String.starts_with ~prefix:"state" "stateless";;
- : bool = true
有些函數,例如 String.starts_with
具有標記參數。當函數具有多個相同類型的參數時,標籤非常有用;命名引數可以讓人猜測其用途。在上方,~prefix:"state"
表示 "state"
作為標記引數 prefix
傳遞。
標記和選擇性參數在標記引數教學中有詳細說明。
有兩種替代方法可以應用函數。
應用運算子
應用運算子 @@
運算子。
# sqrt 9.0;;
- : float = 3.
# sqrt @@ 9.0;;
- : float = 3.
@@
應用運算子會將引數(在右邊)應用到函數(在左邊)。當串聯多個呼叫時,它非常有用,因為它可以避免寫入括號,進而產生更易於閱讀的程式碼。以下是使用和不使用括號的範例
# int_of_float (sqrt (float_of_int (int_of_string "81")));;
- : int = 9
# int_of_float @@ sqrt @@ float_of_int @@ int_of_string "81";;
- : int = 9
管線運算子
管線運算子(|>
)也可以避免括號,但順序相反:函數在右邊,引數在左邊。
# "81" |> int_of_string |> float_of_int |> sqrt |> int_of_float;;
- : int = 9
這就像 Unix Shell 管線一樣。
匿名函數
除非函數是遞迴的,否則不必將函數繫結到名稱。請看這些範例
# fun x -> x;;
- : 'a -> 'a = <fun>
# fun x -> x * x;;
- : int -> int = <fun>
# fun s t -> s ^ " " ^ t ;;
- : string -> string-> string = <fun>
# function [] -> None | x :: _ -> Some x;;
- : 'a list -> 'a option = <fun>
未繫結到名稱的函數值稱為匿名函數。
依照順序,它們是以下內容
- 識別函數,它會取得任何內容並原封不動地回傳
- 平方函數,它會取得一個整數並回傳其平方
- 取得兩個字串並回傳它們之間以空格字元串聯的函數
- 取得一個列表,如果列表為空,則回傳
None
,否則回傳其第一個元素的函數。
匿名函數通常作為引數傳遞給其他函數。
# List.map (fun x -> x * x) [1; 2; 3; 4];;
- : int list = [1; 4; 9; 16]
定義全域函數
您可以使用全域定義將函數全域繫結到名稱。
# let f = fun x -> x * x;;
val f : int -> int = <fun>
碰巧是函數的表達式會變成值並繫結到名稱。以下是執行相同操作的另一種方法
# let g x = x * x;;
val g : int -> int = <fun>
前者會將匿名函數明確繫結到名稱。後者使用更精簡的語法,並避免 fun
關鍵字和箭頭符號。
定義區域函數
可以在區域範圍內定義函數。
# let sq x = x * x in sq 7 * sq 7;;
- : int = 2401
# sq;;
Error: Unbound value sq
呼叫 sq
會發生錯誤,因為它只在區域範圍內定義。
函數 sq
僅在 sq 7 * sq 7
表達式內可用。
儘管區域函數通常定義在函數的作用域內,但這並非必要條件。
閉包
# let j = 2 * 3;;
val j : int = 6
# let k x = x * j;;
val k : int -> int = <fun>
# k 7;;
- : int = 42
# let j = 7;;
val j : int = 7
# k 7;; (* What is the result? *)
- : int = 42
以下說明了其合理性
- 定義了常數
j
,其值為 6。 - 定義了函數
k
。它具有單一參數x
,並回傳x * j
的值。 - 計算
k
的 7,其值為 42 - 建立新的定義
j
,遮蔽第一個定義 - 再次計算
k
的 7,結果相同:42
儘管 j
的新定義遮蔽了第一個定義,但原始定義仍然是函數 k
使用的定義。k
函數的環境會擷取 j
的第一個值,因此每次您應用 k
時(即使在 j
的第二個定義之後),您都可以確信函數的行為相同。
但是,所有未來的表達式都將使用 j
的新值 (7
),如下所示
# let m = j * 3;;
val m : int = 21
將引數部分應用於函數也會建立新的閉包。
# let max_42 = max 42;;
val max_42 : int -> int = <fun>
在 max_42
函數內部,環境包含 max
的第一個參數與值 42 之間的額外繫結。
遞迴函數
為了執行重複的計算,函數可以呼叫自身。這種函數稱為遞迴函數。
# let rec fibo n =
if n <= 1 then n else fibo (n - 1) + fibo (n - 2);;
val fibo : int -> int = <fun>
# let u = List.init 10 Fun.id;;
val u : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]
# List.map fibo u;;
- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34]
這是計算費波那契數的經典(且非常低效)方法。建立的遞迴呼叫次數在每次呼叫時都會加倍,從而產生指數級成長。
在 OCaml 中,必須使用 let rec
定義並明確宣告遞迴函數。不可能意外建立遞迴函數,而且遞迴函數不能是匿名的。
注意:List.init
是一個標準程式庫函數,可讓您透過將給定函數應用於一系列整數來建立列表,而 Fun.id
則是識別函數,它會原封不動地回傳其引數。我們建立了一個數字 0 - 9 的列表,並將其命名為 u
。我們使用 List.map
將 fibo
函數應用於列表的每個元素。
此版本做得更好
# let rec fib_loop m n i =
if i = 0 then m else fib_loop n (n + m) (i - 1);;
val fib_loop : int -> int -> int -> int = <fun>
# let fib = fib_loop 0 1;;
val fib : int -> int = <fun>
# List.init 10 Fun.id |> List.map fib;;
- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34]
第一個版本 fib_loop
有兩個額外的參數:先前計算的兩個費波那契數。
第二個版本 fib
使用前兩個費波那契數作為初始值。從遞迴呼叫回傳時沒有任何需要計算的內容,因此這使編譯器可以執行稱為尾呼叫消除的最佳化。
注意:請注意,fib_loop
函數具有三個參數 m n i
,但是當定義 fib
時,只使用部分應用傳遞了兩個引數 0 1
。
具有多個參數的函數
定義具有多個參數的函數
若要定義具有多個參數的函數,必須在函數的名稱(緊接在 let
關鍵字之後)和等號之間列出每個參數,並以空格分隔
# let sweet_cat x y = x ^ " " ^ y;;
val sweet_cat : string -> string -> string = <fun>
# sweet_cat "kitty" "cat";;
- : string = "kitty cat"
具有多個參數的匿名函數
我們可以使用匿名函數以不同的方式定義相同的函數
# let sour_cat = fun x -> fun y -> x ^ " " ^ y;;
val sour_cat : string -> string -> string = <fun>
# sour_cat "kitty" "cat";;
- : string = "kitty cat"
觀察 sweet_cat
和 sour_cat
具有相同的程式碼主體:x ^ " " ^ y
。它們的差別僅在於列出參數的方式
- 如同
sweet_cat
中名稱和=
之間的x y
- 如同
sour_cat
中=
之後的fun x -> fun y ->
(並且在=
之前只有名稱)
另請注意,sweet_cat
和 sour_cat
具有相同的類型:string -> string -> string
。
如果你使用 compiler explorer 檢查產生的組合語言程式碼,你會發現兩個函式的程式碼是相同的。
sour_cat
的寫法更明確地對應到兩個函式的行為。名稱 sour_cat
綁定到一個匿名函式,該函式帶有一個參數 x
,並回傳另一個匿名函式,該函式帶有一個參數 y
,並回傳 x ^ " " ^ y
。
sweet_cat
的寫法是 sour_cat
的縮寫版本。這種縮短語法的方式稱為語法糖。
部分應用與閉包
我們想要定義類型為 string -> string
的函式,它會在參數前面加上 "kitty "
。這可以使用 sour_cat
和 sweet_cat
來完成
# let sour_kitty x = sour_cat "kitty" x;;
val sour_kitty : string -> string = <fun>
# let sweet_kitty = fun x -> sweet_cat "kitty" x;;
val sweet_kitty : string -> string = <fun>
# sour_kitty "cat";;
- : string = "kitty cat"
# sweet_kitty "cat";;
- : string = "kitty cat"
然而,這兩種定義都可以使用一種稱為部分應用的方式來縮短
# let sour_kitty = sour_cat "kitty";;
val sour_kitty : string -> string = <fun>
# let sweet_kitty = sweet_cat "kitty";;
val sweet_kitty : string -> string = <fun>
由於多參數函式是一系列巢狀的單參數函式,因此您不必一次傳遞所有參數。
將單一參數傳遞給 sour_kitty
或 sweet_kitty
會回傳一個 string -> string
類型的函式。第一個參數,這裡的 "kitty"
,會被捕獲,而結果是一個閉包。
這些表達式具有相同的值
fun x -> sweet_cat "kitty" x
sweet_cat "kitty"
多參數函式的類型
讓我們看看這裡的類型
# let dummy_cat : string -> (string -> string) = sweet_cat;;
val dummy_cat : string -> string -> string = <fun>
這裡使用類型註解 : string -> (string -> string)
來明確聲明 dummy_cat
的類型。
然而,OCaml 回應時聲稱新的定義具有類型 string -> string -> string
。這是因為類型 string -> string -> string
和 string -> (string -> string)
是相同的。
透過括號,很明顯多參數函式是一個單參數函式,它回傳一個刪除一個參數的匿名函式。
以其他方式放置括號是行不通的
# let bogus_cat : (string -> string) -> string = sweet_cat;;
Error: This expression has type string -> string -> string
but an expression was expected of type (string -> string) -> string
Type string is not compatible with type string -> string
類型為 (string -> string) -> string
的函式會將函式作為參數。函式 sweet_cat
的結果是一個函式,而不是將函式作為參數。
類型箭頭運算子向右結合。沒有括號的函式類型應被視為其右側有括號,就像上面宣告的 dummy_cat
類型一樣。只是它們不會顯示出來。
元組作為函式參數
在 OCaml 中,元組是一種資料結構,用於將固定數量的數值分組,這些數值可以是不同的類型。元組用括號括起來,元素之間用逗號分隔。以下是在 OCaml 中建立和使用元組的基本語法
# ("felix", 1920);;
- : string * int = ("felix", 1920)
可以使用元組語法來指定函式參數。以下是如何使用它來定義執行範例的另一個版本
# let spicy_cat (x, y) = x ^ " " ^ y;;
val spicy_cat : string * string -> string = <fun>
# spicy_cat ("hello", "world");;
- : string = "hello world"
看起來好像傳遞了兩個參數:"hello"
和 "world"
。然而,只傳遞了一個,即 ("hello", "world")
元組。檢查產生的組合語言程式碼會顯示它與 sweet_cat
不是同一個函式。它包含更多程式碼。在評估 x ^ " " ^ y
表達式之前,必須先提取傳遞給 spicy_cat
的元組內容 (x
和 y
)。這是額外組合語言指令的作用。
在許多命令式語言中,spicy_cat ("hello", "world")
語法讀作具有兩個參數的函式呼叫;但在 OCaml 中,它表示將函式 spicy_cat
應用於包含 "hello"
和 "world"
的元組。
柯里化與反柯里化
在前面的章節中,已經介紹了兩種多參數函式。
- 回傳函式的函式,例如
sweet_cat
和sour_cat
- 將元組作為參數的函式,例如
spicy_cat
有趣的是,這兩種函式都提供了一種在作為單一參數函式時傳遞多個數據片段的方式。從這個角度來看,可以說:「所有函式都只有一個參數。」
這更進一步。總是可以來回轉換看起來像 sweet_cat
(或 sour_cat
)的函式和看起來像 spicy_cat
的函式。
這些轉換有它們的名稱
- 柯里化從
spicy_cat
形式轉換為sour_cat
(或sweet_cat
)形式。 - 反柯里化從
sour_cat
(或sweet_cat
)形式轉換為spicy_cat
形式。
它也說 sweet_cat
和 sour_cat
是柯里化函式,而 spicy_cat
是反柯里化函式。
具有以下類型的函式可以來回轉換
string -> (string -> string)
— 柯里化函式類型string * string -> string
— 反柯里化函式類型
這些轉換歸功於 20 世紀的邏輯學家 哈斯凱爾·柯里。
在這裡,使用 string
作為範例顯示這一點,但它適用於任何三個類型的群組。
您可以在重構時將柯里化形式更改為反柯里化形式,反之亦然。
但是,也可以從另一個實現一個,以同時提供兩種形式
# let uncurried_cat (x, y) = sweet_cat x y;;
val uncurried_cat : string * string -> string = <fun>
# let curried_cat x y = uncurried_cat (x, y);;
val curried_cat : string -> string -> string = <fun>
實際上,柯里化函式是預設值,因為
- 它們允許部分應用
- 沒有括號或逗號
- 不會對元組進行模式比對
具有副作用的函式
為了解釋副作用,我們需要定義什麼是定義域和值域。讓我們看一個範例
# string_of_int;;
- : int -> string = <fun>
對於函式 string_of_int
- 它的定義域是
int
,即其參數的類型 - 值域是
string
,即其結果的類型
換句話說,定義域在 ->
的左邊,而值域在右邊。這些術語有助於避免說函式類型箭頭的「右邊的類型」或「左邊的類型」。
有些函式會在它們的定義域或值域之外的資料上運作。這種行為稱為效果或副作用。
與作業系統進行輸入和輸出 (I/O) 是最常見的副作用形式。回傳隨機數 (例如 Random.bits
) 或目前時間 (例如 Unix.time
) 的函式的結果會受到外部因素的影響,這也稱為效果。
同樣地,函式計算所觸發的任何可觀察現象都是值域外的輸出。
實際上,什麼被認為是效果是一個工程選擇。在大多數情況下,系統 I/O 操作被認為是效果,除非它們被忽略。處理器在計算函式時發出的熱量通常不被認為是相關的副作用,除非考慮到節能設計。
在 OCaml 社群中,以及在更廣泛的函數式程式設計社群中,函式通常被認為是純函式或不純函式。前者沒有副作用,後者有。這種區別是有意義且有用的。了解效果是什麼,以及它們何時發生,是一個關鍵的設計考量。然而,重要的是要記住這種區別始終假設某種上下文。任何計算都有效果,而什麼被認為是相關的效果是一個設計選擇。
由於根據定義,效果位於函式類型之外,因此函式類型無法反映函式可能的效果。但是,記錄函式預期的副作用非常重要。請考慮 Unix.time
函式。它會回傳自 1970 年 1 月 1 日以來經過的秒數。
# Unix.time ;;
- : unit -> float = <fun>
注意:如果您在 macOS 中收到 Unbound module error
,請先執行此操作:#require "unix";;
。
Unix.time
函式的結果僅由外部因素決定。為了執行副作用,必須將函式套用至引數。由於不需要傳遞任何資料,因此引數是 ()
值。
請考慮 print_endline
。它會將傳遞給它的字串列印到標準輸出,然後是行終止符。
# print_endline;;
- : string -> unit = <fun>
由於函式的目的僅是產生效果,因此它沒有有意義的資料可以回傳;它會回傳 ()
值。
這說明了具有副作用的函式與 unit
類型之間的關係。unit
類型的存在並不表示存在副作用。unit
類型的缺失並不表示沒有副作用。但是,當不需要將任何資料作為輸入傳遞或可以作為輸出回傳時,就會使用 unit
類型。
函式與其他值的不同之處
函式與其他值類似;但是,存在限制
- 函式值無法在互動式會話中顯示。會顯示佔位符
<fun>
來代替。這是因為沒有任何有意義的東西可以列印。一旦剖析並進行類型檢查後,OCaml 會捨棄函式的原始碼,且不會留下任何要列印的東西。
# sqrt;;
- : float -> float = <fun>
- 無法測試函式之間的相等性。
# pred;;
- : int -> int = <fun>
# succ;;
- : int -> int = <fun>
# pred = succ;;
Exception: Invalid_argument "compare: functional value".
有兩個主要原因說明這一點
- 沒有任何演算法可以接收兩個函式,並確定當提供相同的輸入時它們是否會回傳相同的輸出。
- 假設這是可能的,這樣的演算法會宣告快速排序和氣泡排序的實作是相等的。這表示一個可以取代另一個,而這可能不明智。
結論
OCaml 的核心是環境的概念。環境的作用類似於排序的、僅附加的鍵值儲存。這表示無法移除項目。此外,它會透過保留可用定義的順序來維護順序。
當我們使用 let
陳述式時,我們會將零個、一個或多個名稱值對引入到環境中。同樣地,當將函式套用至某些引數時,我們會透過新增與其引數相對應的名稱和值來擴充環境。