OCaml
✍ dations ◷ 2025-02-24 14:04:49 #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,结果的程序可以跨平台编译而在主要平台上无需改动。
相关
- 肝肾综合症肝肾综合征(英语:Hepatorenal syndrome,缩写HRS)是发生于肝硬化或急性肝衰竭患者的致命症状,患者的肾功能会快速恶化。虽然一些治疗方法(如透析)能延缓肝肾综合征的病程,但肝肾综合
- 大清帝国内阁列表本表所列为大清君主立宪制下组织的历届内阁首脑及阁员。张勋复辟时期组织的内阁也附于此表中。中国历史上第一个现代意义的内阁,由庆亲王奕劻组阁。内阁总、协理大臣俱为国务
- 小钱达尔勒·易卜拉欣帕夏钱达尔勒·易卜拉欣帕夏(1429年-1499年,土耳其语:Çandarlı İbrahim Paşa),为了与同名祖父相区分,也被称为小钱达尔勒·易卜拉欣帕夏,奥斯曼帝国官员,出身奥斯曼帝国建国初期显赫
- 米杜柏杯米杜柏杯(Mitropa Cup,也称中欧杯)是一项过去在中欧举办的足球俱乐部际赛事。米杜柏杯被认为是第一项跨国之间俱乐部参加的赛事。其前身是1897年在维也纳举办的挑战杯(Challeng
- 崔光 (朝鲜)崔光(1918年7月17日-1997年2月21日)又名崔明锡,朝鲜民主主义人民共和国开国将领之一、朝鲜劳动党中央政治局委员、中央军事委员会委员。生于咸镜北道罗先市,苏联军事学校专业,曾加
- 花环面包花环面包(德语:Hefekranz / Hefezopf,字面意思为“酵母花环/酵母辫子“)是一种原产瑞士、德国南部、奥地利和意大利南蒂罗尔的面包。面包的面团是由糖、面粉、黄油、鸡蛋 和 酵
- 周浣白周浣白(1912年-1974年),男,安徽桐城人,中华人民共和国军事人物,中国人民解放军少将,曾任中国人民解放军总参谋部通信兵部政治部副主任。
- 阿卜杜勒-哈米德·谢拉夫阿卜杜勒-哈米德·谢拉夫(阿拉伯语:الشريف عبدالحميد شرف,1939年7月8日-1980年7月3日),1979年12月始任约旦总理,7个月后因心梗去世。谢拉夫曾任美国(1967–1972
- 每日放送 (韩国)每日放送(매일방송/每日放送 ;英语:Maeil Broadcasting Network),简称MBN,前身为韩国24小时播送的新闻、财经频道,时称每日经济TV(매일경제TV),
- 光州大邱高速公路 */?),是连接韩国全罗南道潭阳郡和大邱广域市达城郡的一条高速公路,长约183公里。1973年古西JCT-潭阳JCT通车,当时是湖南高速公路的一段。1984年潭阳JCT-玉浦JCT路段通车,古西JCT-潭阳JTC段并入。