簡體   English   中英

在 OCaml 中,`'a.` 和 `type a.` 之間有什么區別以及何時使用它們?

[英]In OCaml, what is the difference between `'a.` and `type a.` and when to use each?

OCaml 有幾種不同的多態類型注解語法:

let f :         'a -> 'a = … (* Isn’t this one already polymorphic? (answer: NO) *)
let f : 'a.     'a -> 'a = …
let f : type a.  a ->  a = …

我們經常在使用花哨的代數數據類型(通常是 GADT)時看到它們,它們似乎是必要的。

這些語法有什么區別? 何時以及為什么必須使用每一個?

以下是具有不同細節的替代解釋,具體取決於您的匆忙程度。;-)

我將使用以下代碼(取自其他問題)作為運行示例。 這里實際上需要reduce定義上的類型注解來進行類型檢查。

(* The type [('a, 'c) fun_chain] represents a chainable list of functions, i.e.
 * such that the output type of one function is the input type of the next one;
 * ['a] and ['c] are the input and output types of the whole chain.
 * This is implemented as a recursive GADT (generalized algebraic data type). *)
type (_, _) fun_chain =
  | Nil  : ('a, 'a) fun_chain
  | Cons : ('a -> 'b) * ('b, 'c) fun_chain -> ('a, 'c) fun_chain

(* [reduce] reduces a chain to just one function by composing all
 * functions of the chain. *)
let rec reduce : type a c. (a, c) fun_chain -> a -> c =
  fun chain x ->
    begin match chain with
    | Nil              -> x
    | Cons (f, chain') -> reduce chain' (f x)
    end

短篇小說

在 let 定義上,像: 'a -> 'a這樣的注釋不會強制多態性:類型檢查器可以將統一變量'a細化為某種東西。 這段語法確實具有誤導性,因為 val 聲明(即模塊簽名中)的相同注釋確實強制執行多態性。

: type a. … : type a. …是一種具有顯式(強制)多態性的類型注釋 您可以將其視為全稱量詞(∀ a ,“對於所有a ”)。 例如,

let some : type a. a -> a option =
  fun x -> Some x

表示“for all”類型a ,你可以給some一個a ,然后它會返回一個a option

這個答案開頭的代碼利用了類型系統的高級特性,即多態遞歸不同類型的分支,這使得類型推斷無所適從。 為了在這種情況下進行程序類型檢查,我們需要像這樣強制多態性。 請注意,在此語法中, a是類型名稱(沒有前導引號)而不是類型統一變量。

: 'a. … : 'a. …是另一種強制多態性的語法,但實際上它包含在: type a. … : type a. …所以你幾乎不需要它。

務實的故事

: type a. … : type a. …是一種結合了兩個特性的簡寫語法:

  1. 顯式多態注釋: 'a. … : 'a. …
    • 有助於確保定義與預期一樣通用
    • 當使用不同於初始調用的類型參數進行遞歸時需要“多態遞歸” ,即“非常規”ADT 上的遞歸)
  2. 本地抽象類型(type a) …
    • 當不同的分支有不同的類型時需要(即“通用” ADT上進行模式匹配時
    • 允許您從定義內部引用類型a ,通常在構建一流模塊時(我不會對此多說)

這里我們使用組合語法,因為我們對reduce的定義屬於粗體兩種情況。

  1. 我們有多態遞歸,因為Cons(b, c) fun_chain (a, c) fun_chain第一個類型參數不同(我們說fun_chain是“非常規”ADT)。

  2. 我們有不同類型的分支,因為Nil構建了一個(a, a) fun_chain ,而Cons構建了一個(a, c) fun_chain (我們說fun_chain是“通用”ADT,簡稱 GADT)。

只是要清楚: : 'a. … : 'a. …: type a. … : type a. …為定義生成相同的簽名。 選擇一種或另一種語法只會影響其正文的類型檢查方式。 對於大多數意圖和目的,您可以忘記: 'a. … : 'a. …記住組合形式: type a. … : type a. … 唉,后者並沒有完全包含前者,很少有寫: type a. … : type a. …不會工作,你需要: 'a. … : 'a. … (見@octachron的回答)但是,希望你不會經常偶然發現它們。

漫長的故事

顯式多態性

OCaml 類型注釋有一個骯臟的小秘密:寫let f : 'a -> 'a = …不會強制f'a中是多態的。 編譯器將提供的注釋與推斷的類型統一起來,並且可以在這樣做的同時自由地實例化類型變量'a ,從而導致類型不如預期的通用。 例如let f : 'a -> 'a = fun x -> x+1是一個被接受的程序並導致val f : int -> int 為確保函數確實是多態的(即,如果定義不夠通用,則讓編譯器拒絕該定義),您必須使用以下語法使多態顯式:

let f : 'a. 'a -> 'a = …

對於非遞歸定義,這僅僅是人類程序員添加了一個約束,這使得更多的程序被拒絕。

然而,在遞歸定義的情況下,這有另一個含義。 在對主體進行類型檢查時,編譯器會將提供的類型與正在定義的函數的所有出現的類型統一起來。 未標記為多態的類型變量將在所有遞歸調用中相等。 但是多態遞歸恰恰是當我們使用不同的類型參數進行遞歸時; 如果沒有顯式的多態性,這將失敗或推斷出比預期更不通用的類型。 為了使其工作,我們明確標記了哪些類型變量應該是多態的。

請注意,OCaml 不能自行對多態遞歸進行類型檢查是有充分理由的:拐角處存在不確定性(參見 Wikipedia 中的參考資料)。

舉個例子,讓我們在這個錯誤的定義上做類型檢查器的工作,其中沒有明確表示多態性:

(* does not typecheck! *)
let rec reduce : ('a, 'c) fun_chain -> 'a -> 'c =
  fun chain x ->
    begin match chain with
    | Nil              -> x
    | Cons (f, chain') -> reduce chain' (f x)
    end

我們從reduce : ('a, 'c) fun_chain -> 'a -> 'cchain : ('a, 'c) fun_chain ,用於某些類型變量'a'c

  • 在第一個分支中, chain = Nil ,所以我們知道實際上是chain : ('c, 'c) fun_chain'a == 'c 我們統一了兩個類型變量。 (不過,現在這並不重要。)

  • 在第二個分支中, chain = Cons (f, chain')所以存在任意類型b使得f : 'a -> b and chain' : (b, 'c) fun_chain 然后我們必須對遞歸調用reduce chain'進行類型檢查,因此預期的參數類型('a, 'c) fun_chain必須與提供的參數類型(b, 'c) fun_chain 但沒有什么告訴我們b == 'a 所以我們拒絕這個定義,最好(按照傳統)帶有一個神秘的錯誤信息:

Error: This expression has type ($Cons_'b, 'c) fun_chain
       but an expression was expected of type ('c, 'c) fun_chain
       The type constructor $Cons_'b would escape its scope

如果現在我們明確多態性:

(* still does not typecheck! *)
let rec reduce : 'a 'c. ('a, 'c) fun_chain -> 'a -> 'c =
  …

然后對遞歸調用進行類型檢查不再是問題,因為我們現在知道reduce是具有兩個“類型參數”(非標准術語)的多態,並且這些類型參數在每次出現時獨立實例reduce 遞歸調用使用b'c ,即使封閉調用使用'a'c

局部抽象類型

但是我們還有第二個問題:構造函數Nil的另一個分支使'a'c統一。 因此,我們最終推斷出比注釋要求的類型更不通用的類型,並且我們報告錯誤:

Error: This definition has type 'c. ('c, 'c) fun_chain -> 'c -> 'c
       which is less general than 'a 'c. ('a, 'c) fun_chain -> 'a -> 'c

解決方案是將類型變量變成局部抽象類型,不能統一(但我們仍然可以有關於它們的類型方程)。 這樣,類型方程在每個分支的本地導出,並且它們不會在match with外部發生。

'a . ... 'a . ...然后type a. ... type a. ...是始終使用后一種形式:

  • type a. ... type a. ...適用於:
    • 多態遞歸
    • GADT
    • 盡早提出類型錯誤

然而:

  • 'a. ... 'a. ...適用於
    • 多態遞歸
    • 行類型變量的多態量化

因此type a. ... type a. ...通常是'a . ... 'a . ... .

除了最后一個奇怪的地方。 為了詳盡起見,讓我舉一個對行類型變量進行量化的示例:

let f: 'a. ([> `X ] as 'a) -> unit = function
  | `X -> ()
  | _ -> ()

在這里,通用量化允許我們精確控制行變量類型。 例如,

let f: 'a. ([> `X ] as 'a) -> unit = function
  | `X | `Y -> ()
  | _ -> ()

產生以下錯誤

Error: This pattern matches values of type [? `Y ] but a pattern was expected which matches values of type [> `X ] The second variant type is bound to the universal type variable 'a, it may not allow the tag(s) `Y

表單type a. ... type a. ...主要是因為本地抽象類型、GADT 類型細化和類型約束的交互尚未正式化。 因此,不支持第二個奇特的用例。

TL;博士; 在您的問題中,只有最后兩種形式是多態類型注釋 這兩種形式中的后者,除了將類型注釋為多態之外,還引入了局部抽象類型1 這是唯一的區別。

更長的故事

現在讓我們談談術語。 以下不是類型注釋(或者,更准確地說,不包含任何類型注釋),

let f :         'a -> 'a = …

它被稱為類型約束 類型約束要求定義值的類型與指定的類型模式兼容

在這個定義中,

let f : 'a.     'a -> 'a = …

我們有一個包含類型注釋的類型約束。 OCaml 用語中的“類型注解”一詞的意思是:用一些信息來注解一個類型,即將一些屬性或屬性附加到一個類型上。 在這種情況下,我們將類型'a注釋為多態的。 我們沒有將值f注釋為多態,我們也沒有將值f注釋為類型'a -> 'a'a. 'a -> 'a 'a. 'a -> 'a 我們將f的值限制為與類型'a -> 'a兼容,並將'a a 注釋為多態類型變量。

長期以來,語法'a. 是將類型注釋為多態的唯一方法,但后來 OCaml 引入了局部抽象類型。 它們具有以下語法,您也可以將其添加到您的集合中。

let f (type t) : t -> t = ...

它創建了一個新的抽象類型構造函數,您可以在定義范圍內使用它。 雖然它沒有將t注釋為多態,所以如果你希望它被顯式注釋為多態,你可以編寫,

let f : 'a. 'a -> 'a = fun (type t) (x : t) : t -> ...

其中包括作為多態的 ' a的顯式類型注釋和本地抽象類型的引入。 不用說,寫這樣的結構很麻煩,所以稍后(OCaml 4.00)他們為此引入了語法糖,這樣上面的表達式就可以簡單地寫成,

let f : type t. t -> t = ...

因此,這種語法只是兩個相當正交的特征的結合:局部抽象類型和顯式多態類型。

然而,這種合並的結果並不比它的部分更強大。 它更像是一個十字路口。 雖然生成的類型既是局部抽象的又是多態的,但它被限制為基本類型。 換句話說,它限制了類型的種類,但這是一個完全不同的高級多態性問題。

總結一下,盡管語法相似,但以下不是類型注釋,

val f : 'a -> 'a

它被稱為值規范,它是簽名的一部分,它表示值f的類型為'a -> 'a


1) ) 本地抽象類型有兩個主要用例。 首先,您可以在表達式中不允許使用類型變量的地方使用它們,例如,在模塊和異常定義中。 其次,本地抽象類型的范圍超出了函數的范圍,您可以通過將表達式的本地類型與抽象類型統一來擴展它們的范圍來使用它。 基本思想是表達式不能超過它的類型,因為在 OCaml 中類型可以在運行時創建,我們也必須小心類型的范圍。 通過函數參數將本地創建的類型與本地抽象類型統一可以保證該類型將與某些現有類型統一,以代替函數應用程序。 直觀地說,這就像傳遞一個類型的引用,以便可以從函數中返回該類型。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM