OCaml
✍ dations ◷ 2025-08-25 03:10:40 #OCaml
OCaml(/oʊˈkæməl/ ),是一个函数式、指令式、模块化、面向对象的通用的编程语言。在Xavier Leroy(英语:Xavier Leroy)和Damien Doligez(英语:Damien Doligez),于1990年和1991年实现的ML方言Caml Light之上,Didier Rémy和Jérôme Vouillon,于1996年增加了面向对象特征,从而形成了“Objective Caml”,在2011年时重命名为“OCaml”。
OCaml工具链包括交互式顶层解释器、字节码编译器、优化的本机代码编译器,可逆调试器和一个包管理器(OPAM)。OCaml最初开发于自动定理证明的场景中,并在静态分析和形式方法软件中有超凡的存在感。此外,它在系统编程、网页编程和金融工程及其他应用领域都有严肃的应用。
历史上,Ascánder Suárez于1987年基于Guy Cousineau(法语:Guy Cousineau)的范畴抽象机器(英语:Categorical abstract machine)(CAM),重新实现了Gérard Huet(英语:Gérard Huet)早先的ML方言,并用“范畴抽象机语言”的首字母简写将其命名为Caml,Caml Light放弃了这个抽象机器又进行了重新实现。OCaml是开放源代码项目,此项目的管理和大部分维护工作,已经交由法国国家信息与自动化研究所(INRIA)。在2000年代早期,来自OCaml的元素被很多语言接纳,特别是F#和Scala。
ML派生语言最著称的是静态类型系统和类型推论编译器。OCaml将函数式、指令式和面向对象编程统一于类ML的类型系统之下。因此编程者不需要为了使用OCaml而非常熟悉纯函数式编程范型。
通过要求编程者在静态类型系统的约束下工作,OCaml消除了关联于动态类型语言的很多有关于类型的运行时间问题。还有,OCaml的类型推论编译器,极大的减少了在多数静态类型语言中对手工类型标注的需要。例如,变量的数据类型和函数的签名,通常不需要像Java和C#语言中那样显式的声明,因为它们可以从应用于这个变量和代码中其他值的算符和其他函数推论出来。有效的使用OCaml的类型系统可能要求一个编程者面对一些复杂性,但是这种规矩能得到可靠的、高性能软件作为回报。
OCaml与源于学术界的其他语言的最显著区别可能是强调了性能。它的静态类型系统防止了运行时间类型不匹配,从而排除了动态类型语言运行时间类型和安全检查的性能负担,却在除了关闭数组边界检查,和使用一些类型不安全特征比如序列化之外的情况下,仍能保证运行时间安全性。这些运行时间检查需求足够罕见,在实践中完全可以避免。
在类型检查开销之外,函数式编程语言,要编译成高效的机器语言代码,由于如函数参数问题(英语:funarg problem)这样的要点,一般而言是具有挑战性的。与标准的循环、寄存器和指令优化(英语:Optimizing compiler)一起,OCaml的优化编译器采用静态程序分析方法,来优化值包装(英语:Object type (object-oriented programming))(boxing)和闭包分配,帮助结果代码得到最大化的性能,即使它大量使用了函数式编程构造。
Xavier Leroy(英语:Xavier Leroy)曾经宣称:“OCaml至少提供了像样的C编译器的50%的性能”,尽管直接比较是不可能的。在OCaml标准库中的一些函数,是采用比在其他语言标准库中等价的函数更快的算法实现的。例如,在OCaml标准库中集合并集的实现,在理论上比指令式语言(例如C++、Java)的标准库中的等价函数,要渐进性的更快,因为OCaml实现利用了集合的不可变性,而在输出中重用了输入集合的一些部分(参见可持久化数据结构)。
OCaml的特征包括:静态类型系统、类型推论、参数多态、尾递归、模式匹配、头等词法闭包、函子(参数化模块)、异常处理和增量分代自动垃圾回收。
OCaml著称于将ML风格类型推论,扩展到通用语言中的对象系统。这允许了结构子类型(英语:Structural type system),这里的对象类型是兼容的,如果它们的方法签名是兼容的,不用管它们声明的继承,这是在静态类型语言中不寻常的特征。
OCaml提供了链接到C原语的外界函数接口(英语:foreign function interface),包括了兼容于C和Fortran二者格式的对高效数值数组的语言支持。OCaml还支持创建可以链接到用C写的main
程序的OCaml函数库。
OCaml发行包含了:
本机代码编译器可以在很多平台上获得,包括Unix、Microsoft Windows和Apple macOS。可移植性是通过至此主要架构的本机代码生成(英语:code generation (compiler))实现的:IA-32、X86-64(AMD64)、Power(英语:Power ISA)、RISC-V、ARM和ARM64。
OCaml字节码和本机代码程序可以用多线程风格书写,具有抢占式上下文切换。但是由于当前唯一可得的语言完全实现INRIA OCaml的垃圾回收器,不是为并发性而设计的,对称多处理是不支持的。在相同进程中的OCaml线程只能分时执行。但是有一些分布式计算库比如Functory和Plasma。
OCaml的代码片段可以通过键入到顶层REPL中来很容易的研习。这是一个交互式的OCaml会话,它打印结果或定义的表达式的推论出的类型。OCaml顶层可以通过简单执行OCaml程序来启动:
$ ocaml OCaml version 4.13.1#
可以接着在#
提示符处键入代码。例如计算1+2*3
:
# 1 + 2 * 3;;- : int = 7
OCaml推论出这个表达式的类型是int
(机器精度整数)并给出结果7
。
下面的程序hello.ml
:
print_endline "Hello World!"
可以被编译成字节码可执行文件:
$ ocamlc hello.ml -o hello
或者被编译成优化的本地代码可执行文件:
$ ocamlopt hello.ml -o hello
接着执行它:
$ ./helloHello World!
给ocamlc的第一个实际参数hello.ml
,指定要编译的源文件而-o hello
标志指定了输出文件。
很多数学函数,比如阶乘,可以很自然的表示为纯粹的函数形式:
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 = %dn" (people+1) else birthday_paradox prob (people+1);;
# birthday_paradox 1.0 1;;answer = 23- : unit = ()
合计整数列表
列表是OCaml中的基础数据类型之一。下面的代码例子定义递归函数sum
,它接受一个实际参数integers
,而它被假定为整数的列表。注意关键字rec
指示了这个函数是递归的。这个函数递归的在给定整数列表之上进行迭代,并提供这些元素的一个总和。match
语句类似于C的switch(英语:Switch statement)
语句,但要更加一般性。
let rec sum integers = (* 关键字rec含义为递归。 *) match integers with | -> 0 (* 产生0,如果integers为空列表 。 *) | first :: rest -> first + sum rest;; (* 递归调用,如果integers是非空列表; first是这个列表的第一个元素, 而rest是余下元素的列表,可能是。 *)
另一种方式是对列表使用标准的fold高阶函数:
let sum integers = List.fold_left (fun accumulator x -> accumulator + x) 0 integers;;
因为匿名函数是简单的+
算符应用,它可以简写为:
let sum integers = List.fold_left (+) 0 integers
进一步的,还可以通过采用部分应用(英语:Partial application)省略列表实际参数:
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 @ @ 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 = xlet succ n f x = f (n f x)let one = succ zerolet 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
而非任何名字来定义。类只充当创建对象的函数,例如上例中的对象可以用类来定义,并接着用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 "%dn" 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 "%dn" 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 ; GlMat.load_identity (); GlMat.rotate ~angle: (angle (Sys.time ())) ~z:1. (); GlDraw.begins `triangles; List.iter GlDraw.vertex2 ; 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,结果的程序可以跨平台编译而在主要平台上无需改动。
相关
- 中山区坐标:25°03′52″N 121°32′00″E / 25.0643611°N 121.533468°E / 25.0643611; 121.533468中山区位于中华民国台北市市中心偏北,此区因孙中山曾于中正区梅屋敷(今国父史迹
- 贝坎贝坎县(Beckham County, Oklahoma)是美国奥克拉荷马州西南部的一个县,西邻德克萨斯州。面积2,344平方公里。根据美国2000年人口普查,共有人口19,799人。县治塞尔 (Sayre)。成立
- 负阻抗变换器负阻抗变换器(negative impedance converter,NIC)是单埠运算放大器电路,有负阻抗特性,会将能量加到电路中,和一般负载消耗能量的情形恰好相反。负阻抗变换器的实现方式是量测一个
- 敖厂长敖厂长(1991年11月10日-),中国大陆网络播客、电子游戏娱乐评论员,活跃于bilibili弹幕网以及YouTube,在bilibili弹幕网拥有约700万粉丝。自称成都养鸡二厂名誉厂长,出生于中国四川成
- 迫田沙织迫田沙织(1987年12月18日-),出生于日本鹿儿岛县鹿儿岛市,是一位日本女子排球运动员,目前效力于东丽箭,场上位置为主攻。因为受到姐姐的影响,她从小学三年级开始接触排球,参加鹿儿岛市
- 杰克·亨特 (广播主持人)小杰克·威廉·亨特(英语:Jack William Hunter, Jr.;1974年6月1日-)是美国的一位广播节目主持人、政治评论家和Rare.us的政治编辑。他也是美国联邦参议员兰德·保罗的支持者。他
- 生丝微菌科生丝微菌科(),是生丝微菌目的一科光合细菌。该属的模式属为生丝微菌属()。
- 泰纳鲁河战役同盟国方面: 美国 英国 亚历山大·范德格里夫特 百武晴吉 美国海军陆战队 大日本帝国陆军泰纳鲁河战役,有时被称为伊鲁河战役,亦或大鳄鱼溪战役,是大日本帝国陆军和美国海军陆战队于太平洋战争期间的1942年8月21日在瓜达尔卡纳尔岛上爆发的一次陆上战役。这是日本陆军在瓜达尔卡纳尔战役期间发动的第一次大规模的陆上攻势。此次战斗中,美国海军陆战队少将亚历山大·范德格里夫特击退了由日本陆军大校一木清直所统领的“一木支队”对岛上美军防线的攻势。当时美国海军陆战队正在守卫伦加防线,这条防线是为了保护在8月7日在瓜
- 吕云章吕云章(1891年9月14日-1974年6月19日),字倬人,亦字沄沁。又称沄、吕小姐,笔名湘灵。山东省福山县留公村人,生于江西省南昌县衙门,其父时任南昌县知县。民国37年(1948年)在山东妇女选区当选第一届立法委员
- 保罗·雷诺斯 (演员)保罗·雷诺斯(Paul Reynolds,1970年2月6日-)是英国的一位演员。他毕业于西尔维亚·杨戏剧学校(英语:Sylvia Young Theatre School)。