热门问题
时间线
聊天
视角

OCaml

Caml程式語言的擴充 来自维基百科,自由的百科全书

Remove ads

OCaml/ˈkæməl/ oh-KAM-əl),是一個函數式指令式模塊化[1]面向對象通用編程語言。在Xavier Leroy英語Xavier LeroyDamien Doligez英語Damien Doligez,於1990年和1991年實現的ML方言Caml Light之上[5],Didier Rémy和Jérôme Vouillon,於1996年增加了面向對象特徵[2],從而形成了「Objective Caml」[6],在2011年時重命名為「OCaml」[7]

事实速览 編程範型, 語言家族 ...
Remove ads

OCaml工具鏈包括交互式頂層解釋器字節碼編譯器、優化的本機代碼編譯器,可逆調試器和一個包管理器(OPAM)。OCaml最初開發於自動定理證明的場景中,並在靜態分析形式方法軟件中有超凡的存在感。此外,它在系統編程網頁編程金融工程及其他應用領域都有嚴肅的應用。

歷史上,Ascánder Suárez於1987年基於Guy Cousineau法語Guy Cousineau範疇抽象機器英語Categorical abstract machine(CAM)[8],重新實現了Gérard Huet英語Gérard Huet早先的ML方言[9],並用「範疇抽象機語言」的首字母簡寫將其命名為Caml[10],Caml Light放棄了這個抽象機器又進行了重新實現[11]。OCaml是開放源代碼項目,此項目的管理和大部分維護工作,已經交由法國國家信息與自動化研究所(INRIA)。在2000年代早期,來自OCaml的元素被很多語言接納,特別是F#Scala

Remove ads

哲學

ML派生語言最著稱的是靜態類型系統類型推論編譯器。OCaml將函數式指令式面向對象編程統一於類ML的類型系統之下。因此編程者不需要為了使用OCaml而非常熟悉純函數式編程范型。

通過要求編程者在靜態類型系統的約束下工作,OCaml消除了關聯於動態類型語言的很多有關於類型的運行時間問題。還有,OCaml的類型推論編譯器,極大的減少了在多數靜態類型語言中對手工類型標註的需要。例如,變量的數據類型和函數的簽名,通常不需要像JavaC#語言中那樣顯式的聲明,因為它們可以從應用於這個變量和代碼中其他值的算符和其他函數推論出來。有效的使用OCaml的類型系統可能要求一個編程者面對一些複雜性,但是這種規矩能得到可靠的、高性能軟件作為回報。

OCaml與源於學術界的其他語言的最顯著區別可能是強調了性能。它的靜態類型系統防止了運行時間類型不匹配,從而排除了動態類型語言運行時間類型和安全檢查的性能負擔,卻在除了關閉數組邊界檢查,和使用一些類型不安全特徵比如序列化之外的情況下,仍能保證運行時間安全性。這些運行時間檢查需求足夠罕見,在實踐中完全可以避免。

在類型檢查開銷之外,函數式編程語言,要編譯成高效的機器語言代碼,由於如函數參數問題英語funarg problem這樣的要點,一般而言是具有挑戰性的。與標準的循環、寄存器和指令優化一起,OCaml的優化編譯器採用靜態程序分析方法,來優化值包裝英語Object type (object-oriented programming)(boxing)和閉包分配,幫助結果代碼得到最大化的性能,即使它大量使用了函數式編程構造。

Xavier Leroy英語Xavier Leroy曾經宣稱:「OCaml至少提供了像樣的C編譯器的50%的性能」[12],儘管直接比較是不可能的。在OCaml標準庫中的一些函數,是採用比在其他語言標準庫中等價的函數更快的算法實現的。例如,在OCaml標準庫中集合併集的實現,在理論上比指令式語言(例如C++、Java)的標準庫中的等價函數,要漸進性的更快,因為OCaml實現利用了集合的不可變性,而在輸出中重用了輸入集合的一些部份(參見可持久化數據結構)。

Remove ads

特徵

OCaml的特徵包括:靜態類型系統類型推論參數多態尾遞歸模式匹配頭等詞法閉包函子(參數化模塊)、例外處理和增量分代自動垃圾回收

OCaml著稱於將ML風格類型推論,擴展到通用語言中的對象系統。這允許了結構子類型英語Structural type system,這裡的對象類型是兼容的,如果它們的方法簽名是兼容的,不用管它們聲明的繼承,這是在靜態類型語言中不尋常的特徵。

OCaml提供了鏈接C原語的外界函數接口英語foreign function interface,包括了兼容於C和Fortran二者格式的對高效數值數組的語言支持。OCaml還支持建立可以鏈接到用C寫的main程序的OCaml函數庫

OCaml發行包含了:

本機代碼編譯器可以在很多平台上獲得,包括UnixMicrosoft WindowsApple macOS。可移植性是通過至此主要架構的本機代碼生成英語code generation (compiler)實現的:IA-32X86-64(AMD64)、Power英語Power ISARISC-VARMARM64[13]

OCaml字節碼和本機代碼程序可以用多線程風格書寫,具有搶占式上下文切換。但是由於當前唯一可得的語言完全實現INRIA OCaml的垃圾回收器,不是為並發性而設計的,對稱多處理是不支持的[14]。在相同進程中的OCaml線程只能分時執行。但是有一些分布式計算庫比如Functory[15]和Plasma[16]

Remove ads

代碼例子

OCaml的代碼片段可以通過鍵入到頂層REPL中來很容易的研習。這是一個交互式的OCaml會話,它打印結果或定義的表達式的推論出的類型[17]。OCaml頂層可以通過簡單執行OCaml程序來啟動:

$ ocaml
OCaml version 5.2.0
Enter #help;; for help.

#

可以接着在#提示符處鍵入代碼。例如計算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 = ()
Remove ads

合計整數列表

列表是OCaml中的基礎數據類型之一。下面的代碼例子定義遞歸函數sum,它接受一個實際參數integers,而它被假定為整數的列表。注意關鍵字rec指示了這個函數是遞歸的。這個函數遞歸的在給定整數列表之上進行迭代,並提供這些元素的一個總和。match語句類似於Cswitch英語Switch statement語句,但要更加一般性。

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

進一步的,還可以通過採用部份應用英語Partial application省略列表實際參數:

let sum =
  List.fold_left (+) 0
Remove ads

快速排序

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
Remove ads

邱奇數

下列代碼定義自然數邱奇編碼,具有後繼(succ)和加法(add)。邱奇數n是接受一個函數f和一個值x的一個高階函數,它應用fx精確的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 : intset_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寫成的程序

參見

有關書籍

引用

外部連結

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads