OCaml
OCaml(/oʊˈkæməl/ oh-KAM-əl),是一個函數式、指令式、模組化[1]、物件導向的通用的程式語言。在Xavier Leroy和Damien Doligez,於1990年和1991年實現的ML方言Caml Light之上[5],Didier Rémy和Jérôme Vouillon,於1996年增加了物件導向特徵[2],從而形成了「Objective Caml」[6],在2011年時重新命名為「OCaml」[7]。
編程範型 | 多範式:函數式,指令式,模組化[1],物件導向[2] |
---|---|
語言家族 | ML |
設計者 | Xavier Leroy, Damien Doligez, Didier Rémy, Jérôme Vouillon |
實作者 | INRIA |
面市時間 | 1996年 |
目前版本 |
|
型態系統 | 靜態,強,類型推論 |
作業系統 | 跨平台 |
授權條款 | GNU較寬鬆公共許可證 |
網站 | ocaml |
衍生副語言 | |
F#, JoCaml, MetaOCaml[4] | |
啟發語言 | |
C, Caml, Modula-2[1], Standard ML | |
影響語言 | |
ATS, Coq, Elm, F#, F*, Haxe, Opa, Rust, Scala |
OCaml工具鏈包括互動式頂層直譯器、位元組碼編譯器、最佳化的本機程式碼編譯器,可逆除錯器和一個包管理器(OPAM)。OCaml最初開發於自動定理證明的場景中,並在靜態分析和形式方法軟體中有超凡的存在感。此外,它在系統編程、網頁編程和金融工程及其他應用領域都有嚴肅的應用。
歷史上,Ascánder Suárez於1987年基於Guy Cousineau的範疇抽象機器(CAM)[8],重新實現了Gérard Huet早先的ML方言[9],並用「範疇抽象機語言」的首字母簡寫將其命名為Caml[10],Caml Light放棄了這個抽象機器又進行了重新實現[11]。OCaml是開放原始碼專案,此專案的管理和大部分維護工作,已經交由法國國家資訊與自動化研究所(INRIA)。在2000年代早期,來自OCaml的元素被很多語言接納,特別是F#和Scala。
哲學
ML衍生語言最著稱的是靜態型別系統和類型推論編譯器。OCaml將函數式、指令式和物件導向程式設計統一於類ML的型別系統之下。因此編程者不需要為了使用OCaml而非常熟悉純函數式程式設計範式。
通過要求編程者在靜態型別系統的約束下工作,OCaml消除了關聯於動態型別語言的很多有關於類型的執行時間問題。還有,OCaml的類型推論編譯器,極大的減少了在多數靜態型別語言中對手工類型標註的需要。例如,變數的資料類型和函式的簽章,通常不需要像Java和C#語言中那樣顯式的聲明,因為它們可以從應用於這個變數和程式碼中其他值的算符和其他函式推論出來。有效的使用OCaml的型別系統可能要求一個編程者面對一些複雜性,但是這種規矩能得到可靠的、高效能軟體作為回報。
OCaml與源於學術界的其他語言的最顯著區別可能是強調了效能。它的靜態型別系統防止了執行時間類型不匹配,從而排除了動態型別語言執行時間類型和安全檢查的效能負擔,卻在除了關閉陣列邊界檢查,和使用一些類型不安全特徵比如序列化之外的情況下,仍能保證執行時間安全性。這些執行時間檢查需求足夠罕見,在實踐中完全可以避免。
在型別檢查開銷之外,函數式程式設計語言,要編譯成高效的機器語言程式碼,由於如函式參數問題這樣的要點,一般而言是具有挑戰性的。與標準的迴圈、暫存器和指令最佳化一起,OCaml的最佳化編譯器採用靜態程式分析方法,來最佳化值包裝(boxing)和閉包分配,幫助結果程式碼得到最大化的效能,即使它大量使用了函數式程式設計構造。
Xavier Leroy曾經宣稱:「OCaml至少提供了像樣的C編譯器的50%的效能」[12],儘管直接比較是不可能的。在OCaml標準庫中的一些函式,是採用比在其他語言標準庫中等價的函式更快的演算法實現的。例如,在OCaml標準庫中集合併集的實現,在理論上比指令式語言(例如C++、Java)的標準庫中的等價函式,要漸進性的更快,因為OCaml實現利用了集合的不可變性,而在輸出中重用了輸入集合的一些部份(參見可持久化資料結構)。
特徵
OCaml的特徵包括:靜態型別系統、類型推論、參數多型、尾遞迴、模式匹配、頭等詞法閉包、函子(參數化模組)、例外處理和增量分代自動垃圾回收。
OCaml著稱於將ML風格類型推論,擴充到通用語言中的對象系統。這允許了結構子類型,這裡的對象類型是相容的,如果它們的方法簽章是相容的,不用管它們聲明的繼承,這是在靜態型別語言中不尋常的特徵。
OCaml提供了連結到C原語的外界函式介面,包括了相容於C和Fortran二者格式的對高效數值陣列的語言支援。OCaml還支援建立可以連結到用C寫的main
程式的OCaml函式庫。
OCaml發行包含了:
本機程式碼編譯器可以在很多平台上獲得,包括Unix、Microsoft Windows和Apple macOS。可移植性是通過至此主要架構的本機程式碼生成實現的:IA-32、X86-64(AMD64)、Power、RISC-V、ARM和ARM64[13]。
OCaml位元組碼和本機程式碼程式可以用多執行緒風格書寫,具有搶占式上下文交換。但是由於當前唯一可得的語言完全實現INRIA OCaml的垃圾回收器,不是為並行性而設計的,對稱多處理是不支援的[14]。在相同行程中的OCaml執行緒只能分時執行。但是有一些分散式計算庫比如Functory[15]和Plasma[16]。
程式碼範例
OCaml的程式碼片段可以通過鍵入到頂層REPL中來很容易的研習。這是一個互動式的OCaml對談,它列印結果或定義的表達式的推論出的類型[17]。OCaml頂層可以通過簡單執行OCaml程式來啟動:
$ ocaml
OCaml version 4.13.1
#
可以接著在#
提示符處鍵入程式碼。例如計算1+2*3
:
# 1 + 2 * 3;;
- : int = 7
OCaml推論出這個表達式的類型是int
(機器精度整數)並給出結果7
。
Hello World
下面的程式hello.ml
:
print_endline "Hello World!"
可以被編譯成位元組碼可執行檔:
$ ocamlc hello.ml -o hello
或者被編譯成最佳化的本地程式碼可執行檔:
$ ocamlopt hello.ml -o hello
接著執行它:
$ ./hello
Hello World!
給ocamlc的第一個實際參數hello.ml
,指定要編譯的原始檔而-o hello
標誌指定了輸出檔案[18]。
階乘函式
很多數學函式,比如階乘,可以很自然的表示為純粹的函式形式:
let rec fact n =
if n=0 then 1 else n * fact(n - 1);;
這個函式可以使用模式匹配等價的寫為:
let rec fact = function
| 0 -> 1
| n -> n * fact(n - 1);;
後者形式是階乘作為遞推關係的數學定義。
編譯器將這個函式的類型推論為int -> int
,意味著這個函式將int
對映到int
。例如,12!
:
# fact 12;;
- : int = 479001600
斐波那契序列
下列程式碼計算輸入數n
的斐波那契數列。它使用了尾遞迴和模式匹配。
let fib n =
let rec fib_aux m a b =
match m with
| 0 -> a
| _ -> fib_aux (m - 1) b (a + b)
in fib_aux n 0 1;;
生日問題
下列程式計算在一個屋子裡面有完全唯一的生日概率小於50%的最少人數,在生日問題中,對於1個人這個概率是365/365(或100%),對於2個人是364/365,對於3個人是364/365 × 363/365,最終答案是23個人:
let year_size = 365.
let rec birthday_paradox prob people =
let prob = (year_size -. float people) /. year_size *. prob in
if prob < 0.5 then
Printf.printf "answer = %d\n" (people+1)
else
birthday_paradox prob (people+1);;
# birthday_paradox 1.0 1;;
answer = 23
- : unit = ()
合計整數列表
列表是OCaml中的基礎資料類型之一。下面的程式碼範例定義遞迴函式sum
,它接受一個實際參數integers
,而它被假定為整數的列表。注意關鍵字rec
指示了這個函式是遞迴的。這個函式遞迴的在給定整數列表之上進行迭代,並提供這些元素的一個總和。match
語句類似於C的switch
語句,但要更加一般性。
let rec sum integers = (* 关键字rec含义为递归。 *)
match integers with
| [] -> 0 (* 产生0,如果integers为空列表 []。 *)
| first :: rest -> first + sum rest;; (* 递归调用,如果integers是非空列表;
first是这个列表的第一个元素,
而rest是余下元素的列表,可能是[]。 *)
# sum [1;2;3;4;5];;
- : int = 15
另一種方式是對列表使用標準的fold高階函式:
let sum integers =
List.fold_left (fun accumulator x -> accumulator + x) 0 integers;;
# sum [1;2;3;4;5];;
- : int = 15
因為匿名函式是簡單的+
算符應用,它可以簡寫為:
let sum integers =
List.fold_left (+) 0 integers
進一步的,還可以通過採用部份應用省略列表實際參數:
let sum =
List.fold_left (+) 0
快速排序
OCaml自身可提供對遞迴演算法的簡介表達。下列程式碼範例實現了類似於以升序排序一個列表的quicksort的一個演算法:
let rec qsort = function
| [] -> []
| pivot :: rest ->
let is_less x = x < pivot in
let left, right = List.partition is_less rest in
qsort left @ [pivot] @ qsort right;;
高階函式
函式可以接受函式作為參數並且返回函式作為結果。例如,應用twice
到函式f
產生應用f
到它的實際參數兩次的一個函式:
let twice (f : 'a -> 'a) = fun (x : 'a) -> f (f x);;
let inc (x : int) : int = x + 1;;
let add2 = twice inc;;
let inc_str (x : string) : string = x ^ " " ^ x;;
let add_str = twice(inc_str);;
# add2 98;;
- : int = 100
# add_str "Test";;
- : string = "Test Test Test Test"
函式twice
使用類型變數'a
,來指示它可以應用於對映一個類型'a
到自身的任何f
,而非只應用於int->int
函式。特別是,twice
甚至可以應用於自身:
# let fourtimes f = (twice twice) f;;
val fourtimes : ('a -> 'a) -> 'a -> 'a = <fun>
# let add4 = fourtimes inc;;
val add4 : int -> int = <fun>
# add4 98;;
- : int = 102
邱奇數
下列程式碼定義自然數的邱奇編碼,具有後繼(succ)和加法(add)。邱奇數n
是接受一個函式f
和一個值x
的一個高階函式,它應用f
到x
精確的n
次:
let zero f x = x
let succ n f x = f (n f x)
let one = succ zero
let two = succ (succ zero)
let add n1 n2 f x = n1 f (n2 f x)
let to_string n = n (fun k -> "S" ^ k) "0";;
為了將一個邱奇數從函式值轉換成一個字串,這裡把它傳遞給向其輸入和常數字串"0"
前置上字串"S"
的函式to_string
:
# let _ = to_string (add (succ two) two);;
- : string = "SSSSS0"
對象範例
在OCaml中對象,通過它們的方法的名字和類型,按結構來確定類型。對象可以直接建立(立即對象),而不用通過有名稱的類。例如:
# let x =
object
val mutable x = 5
method get_x = x
method set_x y = x <- y
end;;
val x : < get_x : int; set_x : int -> unit > = <obj>
這裡OCaml互動式執行時間系統列印出這個對象的推論類型。它的類型< get_x : int; set_x : int -> unit >
,只由它的方法來定義。換句話說,x
的類型由方法類型get_x : int
和set_x : int -> unit
而非任何名字來定義[19]。類只充當建立對象的函式,例如上例中的對象可以用類來定義,並接著用new
算符來建立:
# class simple_cls =
object (self)
val mutable x = 5
method get_x = x
method set_x y = x <- y
end;;
let x = new simple_cls;;
要定義有相同的方法和方法類型的另一個對象:
# let y =
object
method get_x = 2
method set_x y = Printf.printf "%d\n" y
end;;
val y : < get_x : int; set_x : int -> unit > = <obj>
OCaml將它們視為有相同的類型。例如,等式算符被確定類型為只接受有相同類型的兩個值:
# x = y;;
- : bool = false
所有儘管它們有不同的值,卻必定是相同類型的,否則連型別檢查都不會做完。這展示了類型等價是結構性的。可以定義呼叫一個方法的函式:
# let set_to_10 a = a#set_x 10;;
val set_to_10 : < set_x : int -> 'a; .. > -> 'a = <fun>
第一個實際參數的推論類型< set_x : int -> 'a; .. >
是值得關注的。..
意味著第一個實際參數,可以是有接受一個int
作為實際參數的set_x
方法的任何對象。所以它可以用在對象x
之上:
# set_to_10 x;;
- : unit = ()
另一個對象可以碰巧有這個方法和方法類型;其他方法是無關緊要的:
# let z =
object
method blahblah = 2.5
method set_x y = Printf.printf "%d\n" y
end;;
val z : < blahblah : float; set_x : int -> unit > = <obj>
set_to_10
函式對它也有效:
# set_to_10 z;;
10
- : unit = ()
這展示了對於事物比如方法呼叫的相容性是由結構來確定的。下面為只有一個get_x
方法而沒有其他方法的對象定義一個類型同義詞(synonym):
# type simpler_obj = < get_x : int >;;
type simpler_obj = < get_x : int >
對象x
不是這個類型的;但在結構上x
是這個類型的一個子類型,因為x
包含它的方法的一個超集。所以x
可以強制(coerce)成這個類型:
# (x :> simpler_obj);;
- : simpler_obj = <obj>
# (x :> simpler_obj)#get_x;;
- : int = 10
但是對象z
不行,因為它不是一個結構子類型:
# (z :> simpler_obj);;
Error: Type < blahblah : float; set_x : int -> unit > is not a subtype of
simpler_obj = < get_x : int >
The first object type has no method get_x
這展示了拓寬強制的相容性是結構性的。
任意精度階乘函式
可以從OCaml訪問各種各樣的庫。比如,OCaml有內建的任意精度算術。由於階乘函式增長得非常迅速,會很快溢位機器精度的整數。因此階乘很適合選用任意精度算術。
在OCaml中,Num
模組(現在被ZArith
所取代)提供了任意精度算術,比如在Ubuntu中安裝它:sudo apt install libnum-ocaml-dev
,它可以如下這樣裝載到執行中的頂層中:
#use "topfind";;
#require "num";;
open Num;;
階乘函式可以使用任意精度算符=/
、*/
和-/
寫為:
let rec fact n =
if n =/ Int 0 then Int 1 else n */ fact(n -/ Int 1);;
這個函式可以計算非常大的階乘比如120!
:
# string_of_num (fact (Int 120));;
- : string =
"6689502913449127057588118054090372586752746333138029810295671352301633557244962989366874165271984981308157637893214090552534408589408121859898481114389650005964960521256960000000000000000000000000000"
繪製圖形範例
下列程式simple.ml
使用OpenGL呈現一個緩慢旋轉的2D三角形:
let () =
ignore (Glut.init Sys.argv);
Glut.initDisplayMode ~double_buffer:true ();
ignore (Glut.createWindow ~title:"OpenGL Demo");
let angle t = 10. *. t *. t in
let render () =
GlClear.clear [ `color ];
GlMat.load_identity ();
GlMat.rotate ~angle: (angle (Sys.time ())) ~z:1. ();
GlDraw.begins `triangles;
List.iter GlDraw.vertex2 [-1., -1.; 0., 1.; 1., -1.];
GlDraw.ends ();
Glut.swapBuffers () in
GlMat.mode `modelview;
Glut.displayFunc ~cb:render;
Glut.idleFunc ~cb:(Some Glut.postRedisplay);
Glut.mainLoop ()
需要事先安裝負責繫結到OpenGL的LablGL,比如在Ubuntu中安裝它:sudo apt install liblablgl-ocaml-dev
,這個程式可以如下這樣編譯成位元組碼:
$ ocamlc -I +lablGL lablglut.cma lablgl.cma simple.ml -o simple
或編譯成本機程式碼:
$ ocamlopt -I +lablGL lablglut.cmxa lablgl.cmxa simple.ml -o simple
或者簡單的使用ocamlfind
建造命令:
$ ocamlfind opt simple.ml -package lablgl.glut -linkpkg -o simple
然後執行:
$ ./simple
可以使用OCaml開發非常複雜、高效能的2D和3D圖形程式。使用OpenGL和OCaml,結果的程式可以跨平台編譯而在主要平台上無需改動。
用OCaml寫成的程式
- Coq,一個形式證明輔助器[20]。
- HOL Light,一個形式證明輔助器。
- Coccinelle,用於C程式原始碼變換的實用工具。
- Frama-C,分析C程式的一個框架。
- Flow,Facebook建立的用於推論和檢查JavaScript的靜態型別的一個靜態剖析器[21]。
- Infer,Facebook建立的用於Java、C、C++和Objective-C的靜態剖析器,用來檢測iOS和Android應用中的缺陷[22]。
- pyre-check,Facebook建立的用於Python的型別檢查器[23]。
- Hack程式語言編譯器,Facebook建立,擴充PHP具有靜態型別。
- Haxe程式語言編譯器。
- WebAssembly參考直譯器,它是意圖在web瀏覽器內執行的低層位元組碼[24]。
- FFTW,C的FFT庫,它的多數效能關鍵程式碼是由用OCaml寫的一個程式生成的[25]。
- Owl科學計算,用於科學和工程計算的專用系統。
- MirageOS,用純OCaml寫的一個單一核心編程框架[26]。
- Ocsigen,一個OCaml的web應用框架。
- Opa,用於Web開發的自由和開源的一個程式語言。
- 0install,一個多平台包管理器。
- MLDonkey,一個多網路P2P程式。
- Unison,一個檔案同步器[27]。
參見
有關書籍
- Yaron Minsky, Anil Madhavapeddy. Real World OCaml, Functional programming for the masses, 2nd Edition. 2021 [2021-09-10]. (原始內容存檔於2022-05-14).
- Emmanuel Chailloux, Pascal Manoury, Bruno Pagano. Developing Applications With Objective Caml (PDF). 2000 [2021-09-10]. (原始內容 (PDF)存檔於2022-02-23).
- Jason Hickey. Introduction to the Objective Caml Programming Language. 2002 [2021-09-10]. (原始內容存檔於2021-09-10).
參照
- ^ 1.0 1.1 1.2 Xavier Leroy. Manifest types, modules, and separate compilation (PDF). Principles of Programming Languages. 1994 [2021-09-06]. (原始內容 (PDF)存檔於2021-10-22).
- ^ 2.0 2.1 Didier Rémy. Type inference for records in a natural extension of ML. Research Report RR-1431, INRIA. 1991 [2021-09-10]. (原始內容存檔於2022-04-06).
Didier Rémy, Jérôme Vouillon. Objective ML: a simple object-oriented extension of ML (PDF). 1997 [2021-09-06]. (原始內容 (PDF)存檔於2022-01-21).
Didier Rémy, Jérôme Vouillon. Objective ML: An effective object-oriented extension to ML (PDF). 1998 [2021-09-06]. (原始內容 (PDF)存檔於2022-01-20). - ^ OCaml 5.2.0 Release Notes. [2024年5月24日].
- ^ MetaOCaml -- an OCaml dialect for multi-stage programming. [2021-08-28]. (原始內容存檔於2021-08-29).
- ^ Xavier Leroy. The Caml Light system Release 0.74, documentation and user's guide. 1997 [2021-09-02]. (原始內容存檔於2022-03-08).
- ^ Xavier Leroy. The Objective Caml system release 1.07, Documentation and user's manual. 1997 [2021-09-02]. (原始內容存檔於2022-01-23).
- ^ A History of Caml. [2021-09-02]. (原始內容存檔於2022-04-13).
Our main reason for developing Caml was to use it for sofware development inside Formel. Indeed, it was used for developing the Coq system ……. We were reluctant to adopt a standard that could later prevent us from adapting the language to our programming needs. ……We did incorporate into Caml most of the improvements brought by Standard ML over Edinburgh ML. ……The first implementation of Caml appeared in 1987 and was further developed until 1992. It was created mainly by Ascander Suarez. ……
In 1990 and 1991, Xavier Leroy designed a completely new implementation of Caml, based on a bytecode interpreter written in C. Damien Doligez provided an excellent memory management system. ……In 1995, Xavier Leroy released Caml Special Light, which improved over Caml Light in several ways. In 1995, Xavier Leroy released Caml Special Light, which improved over Caml Light in several ways. First, an optimizing native-code compiler was added to the bytecode compiler. ……Second, Caml Special Light offered a high-level module system, designed by Xavier Leroy and inspired by the module system of Standard ML. ……Didier Rémy, later joined by Jérôme Vouillon, designed an elegant and highly expressive type system for objects and classes. This design was integrated and implemented within Caml Special Light, leading to the Objective Caml language and implementation, first released in 1996 and renamed to OCaml in 2011. - ^ G. Cousineau, P.-L. Curien, M. Mauny. The categorical abstract machine. 1985 [2021-09-03]. (原始內容存檔於2021-09-03). LNCS, 201, Functional programming languages computer architecture, pp.~50-64.
Michel Mauny, Ascánder Suárez. Implementing functional languages in the Categorical Abstract Machine (PDF). 1986 [2021-09-07]. (原始內容 (PDF)存檔於2022-01-28). LFP '86: Proceedings of the 1986 ACM conference on LISP and functional programming, Pages 266–278. - ^ G. Cousineau, M. Gordon, G. Huet, R. Milner, L. C. Paulson, C. Wadsworth. The ML Handbook, Version 6.2. Internal document. Project Formel, INRIA. July 1985.
Christoph Kreitz, Vincent Rahli. Introduction to Classic ML (PDF). 2011 [2021-09-11]. (原始內容 (PDF)存檔於2022-01-29).This handbook is a revised edition of Section 2 of 『Edinburgh LCF』, by M. Gordon, R. Milner, and C. Wadsworth, published in 1979 as Springer Verlag Lecture Notes in Computer Science no 78. ……The language is somewhere in between the original ML from LCF and standard ML, since Guy Cousineau added the constructors and call by patterns. This is a LISP based implementation, compatible for Maclisp on Multics, Franzlisp on VAX under Unix, Zetalisp on Symbolics 3600, and Le Lisp on 68000, VAX, Multics, Perkin-Elmer, etc... Video interfaces have been implemented by Philippe Le Chenadec on Multics, and by Maurice Migeon on Symbolics 3600. The ML system is maintained and distributed jointly by INRIA and the University of Cambridge.
- ^ Guy Cousineau, Gérard Huet. The CAML primer Version 2.6.1. 1990 [2021-09-07]. (原始內容存檔於2022-05-04). RT-0122, INRIA. pp.78.
Pierre Weis, Maria Virginia Aponte, Alain Laville, Michel Mauny, Ascander Suarez. The CAML reference manual Version 2.6.1. 1990 [2021-09-07]. (原始內容存檔於2022-04-06). [Research Report] RT-0121, INRIA. pp.491. - ^ Xavier Leroy. The ZINC experiment : an economical implementation of the ML language. 1990 [2021-09-06]. (原始內容存檔於2022-04-06). RT-0117, INRIA.
- ^ Linux Weekly News (頁面存檔備份,存於網際網路檔案館).
- ^ ocaml/asmcomp at trunk · ocaml/ocaml · GitHub. GitHub. [2 May 2015]. (原始內容存檔於2022-05-07).
- ^ Archives of the Caml mailing list > Message from Xavier Leroy. [2021-09-10]. (原始內容存檔於2022-03-31).
- ^ Functory. [2021-08-30]. (原始內容存檔於2022-01-20).
- ^ ocamlnet/Plasma. [2021-08-30]. (原始內容存檔於2022-03-23).
- ^ OCaml - The toplevel system or REPL (ocaml). ocaml.org. [2021-05-17]. (原始內容存檔於2021-09-11).
- ^ Batch compilation (ocamlc) (頁面存檔備份,存於網際網路檔案館).
- ^ Object types. [2021-09-10]. (原始內容存檔於2022-03-10).
- ^ Coq(頁面存檔備份,存於網際網路檔案館)
- ^ Flow: A Static Type Checker for JavaScript. [2021-08-29]. (原始內容存檔於2022-04-08).
- ^ Infer static analyzer. [2021-08-29]. (原始內容存檔於2022-05-12).
- ^ GitHub - facebook/pyre-check: Performant type-checking for python.. 9 February 2019 [2021-08-29]. (原始內容存檔於2022-05-05) –透過GitHub.
- ^ WebAssembly/spec: WebAssembly specification, reference interpreter, and test suite.. World Wide Web Consortium. 5 December 2019 [2021-05-14]. (原始內容存檔於2022-05-12) –透過GitHub.
- ^ FFTW(頁面存檔備份,存於網際網路檔案館)
- ^ MirageOS. [2021-08-29]. (原始內容存檔於2022-04-20).
- ^ Unison. [2021-08-28]. (原始內容存檔於2021-07-12).