基础

运行 OCaml 代码

运行OCaml代码的最简单的方式是在你的浏览器中使用https://ocsigen.org/js_of_ocaml/2.7/files/toplevel/index.html运行一个交互式会话。

关于在你的电脑上安装OCaml,请参考安装文档。

如果你想要尝试一些简单的OCaml表达式,你可以使用一个交互式顶层环境(interactive toplevel)或者REPL(Read–Eval–Print Loop)。ocaml命令提供了一个十分基础的顶层环境(你应该使用包管理器安装rlwrap ,通过运行rlwrap ocaml来获取历史导航 )。我们推荐使用utop顶层环境,如果你可以通过系统的包管理器或者OPAM来安装它。utop具有与OCaml标准顶层环境相同的基础接口,同时更易于使用(具有历史导航、自动完成等特性)。

使用;; 来表示你完成了一个表达式的输入,在顶层环境中如下所示:

$ ocaml
        OCaml version 4.10.0

# 1+1;;
- : int = 2

使用utop运行程序的效果如下:

───────┬────────────────────────────────────────────────────────────┬─────
       │ Welcome to utop version 1.18 (using OCaml version 4.02.3)! │     
       └────────────────────────────────────────────────────────────┘     

Type #utop_help for help about using utop.

─( 10:12:16 )─< command 0 >───────────────────────────────────────────────
utop # 1 + 1;;
- : int = 2

编译名为my_prog.ml的OCaml源码文件为一个原生代码可执行程序,可以使用ocamlbuild my_prog.native命令:

$ mkdir my_project
$ cd my_project
$ echo 'let () = print_endline "Hello, World!"' > my_prog.ml
$ ocamlbuild my_prog.native
Finished, 4 targets (0 cached) in 00:00:00.
$ ./my_prog.native
Hello, World!

详细内容请参考Compiling OCaml projects

注释

OCaml的注释是用(**)括起的,如:

(* 这是一个单行的注释. *)

(* 这是一个
 * 多行
 * 的注释.
 *)

这和C的注释(/* ... */)很相似。 目前还没有像Perl中# ...或C99/C++/Java中// ...那样的单行注释方式。

OCaml中可以使用嵌套(* ... *)块, 因此我们可以很容易地注释掉某一块程序:

(* This code is broken ...

(* Primality test. *)
let is_prime n =
  (* note to self: ask about this on the mailing lists *) XXX;;
*)

调用函数

假设你已经写好了一个函数repeated,它的参数是一个字符串s和一个数n,返回值是把s重复n遍形成的新字符串。

在大多数源于C的语言中,调用这一函数会象下面这样:

repeated ("hello", 3)  /* this is C code */

这意味着“调用函数repeated, 输入两个参数,第一是字符串hello,第二是数字3”。

OCaml和其他函数式语言一样以另外的方式来调用函数, 初学者的很多错误是因为忽视了这一点。下面是OCaml中的函数调用:

repeated "hello" 3  (* this is OCaml code *)

注意:这里没有括号, 参数中间没有逗号。

容易使人困惑的是repeated ("hello", 3) 在OCaml中是合法的的。它意味着“调用函数repeated,输入一个参数,这一个参数是一个含两个元素的对(pair)”。这当然不符合原意,因为repeated函数需要的参数是两个而不是一个,而且第一个参数要求是字符串而不是对。 这里我们暂时无需知道什么是对(pair),我们只需记住用括号括起参数和用逗号分隔参数是错误的。

我们来看另一个函数prompt_string,它输出字符串提示,并返回一个用户输入的字符串。我们想把这个返回的字符串输入repeated.下面是C和OCaml的两种版本:

/* C code: */
repeated (prompt_string ("Name please: "), 3)
(* OCaml code: *)
repeated (prompt_string "Name please: ") 3

请注意括号的不同和逗号的有无。在OCaml的版本,prompt_string的返回值由括号括起,作为第一个参数传入。一般情况下,规则是:“括号只括起整个函数调用,不要括起函数调用的参数。”下面是更多的例子:

f 5 (g "hello") 3    (* f has three arguments, g has one argument *)
f (g 3 4)            (* f has one argument, g has two arguments *)
# let repeated (s: string) (i: int) =
    failwith "implementation not given";;
val repeated : string -> int -> 'a = <fun>
# repeated ("hello", 3)     (* OCaml will spot the mistake *);;
Error: This expression has type 'a * 'b but an expression was expected of type string

函数定义

大家应该都知道在我们已经学会的语言中怎么定义一个函数(或java中的静态方法),那么OCaml中怎么做呢?

OCaml的语法很简洁。下面是一个函数输入两个浮点数后计算它们的平均值:

let average a b =
  (a +. b) /. 2.0;;

把它输入OCaml的toplevel(在Unix中,在shell中输入命令ocaml)中后你会看到:

# let average a b =
    (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

如果你仔细看这个函数定义和OCaml的返回,你会发现一些问题:

  • 代码中那些运算符后多出的点号是什么?
  • float -> float -> float是什么意思?

我将在以后章节回答这些问题,但首先我想来用C(Java的定义和C很类似)来定义同样的函数,希望这会使大家产生更多的问题。下面是C版的average:

double average (double a, double b)
{
  return (a + b) / 2;
}

现在再看上面短得多的OCaml版。可能大家会问:

  • 为什么在OCaml中无需定义ab的类型?OCaml怎么知道它们的类型?或者OCaml是否知道它们的类型呢?难道OCaml是完全动态类型的语言吗?
  • C中2隐式转换成double类型, 可OCaml为什么不这样做?
  • OCaml中return的方式是怎样?

我们来看答案:

  • OCaml是强静态类型的语言。(也就是说没有如perl中的动态类型)。
  • OCaml用类型推导(type inference)来找出类型,所以大家无需注明类型。如果你用上述OCaml的toplevel,那么OCaml会显示出它认为的函数类型。
  • OCaml不做任何的隐式转换。如果你需要浮点数,你必须写2.0,因为2是一个整数。OCaml从不执行任何自动类型转换。
  • 由于type inference的副作用,OCaml不允许任何形式的重载(包括操作符重载)。它用不同的运算符来表示“两个整数相加”(用+)和“两个浮点数相加”(用+.)。注意后者有一个点号。其他算术运算符( -.*./. )也是这样。
  • OCaml返回函数的最后的表达式值,因此我们没有必要如C中一样写return

更多的细节将稍后详述。

基本类型

OCaml中的基本类型是:

OCaml type  Range

int         31-bit signed int (roughly +/- 1 billion) on 32-bit
            processors, or 63-bit signed int on 64-bit processors
float       IEEE double-precision floating point, equivalent to C's double
bool        A boolean, written either true or false
char        An 8-bit character
string      A string
unit        Written as ()

OCaml内部使用int中的一位来自动管理内存(垃圾收集)。因此基本 int类型是31位而非32位(如果你用64位平台,那就是63位)。在实际应用中,大多数情况下这不是问题。例如在循环计数中,OCaml中只能数到10亿而不是20亿。这并不成为问题,因为如果你要很大的计数你应该使用大数模块(NatBig_int模块)。但如果你的应用一定需要处理32位类型(比如你要写一些加密程序或者网络协议栈),OCaml提供nativeint类型。

OCaml基本类型中没有提供无符号整数类型。但是你可以使用nativeint来达到同样效果。另外就我所知,OCaml没有单精度浮点数。

OCaml提供char类型来表示字符。但可惜的是char类型不支持Unicode或者UTF-8。这是一个需要改进的很严重的缺点。但是当前我们可以使用 comprehensive Unicode libraries来处理。

字符串并非只是字符的链表,它们有自己更高效的内部表示方式。

unit类型有点象C中的void类型,我们会稍后详述。

隐式转换和显式转换的比较

在源于C的语言中,int类型在某些情况下会自动提升成浮点类型。例如你写1 + 2.5那么第一个参数(整数类型)会提升成浮点数,计算结果也是一个浮点数。这等价于你写了((double) 1) + 2.5,在这里自动执行了隐式转换。

OCaml从不执行隐式转换。在OCaml中,1 + 2.5犯了类型错误。操作符+要求两个整数作为参数,而我们这里给出了一个整数一个浮点数,因此它会报错如下:

# 1 + 2.5;;
Error: This expression has type float but an expression was expected of type int

(这段错误信息是“法式英语”。意思是这里你给了浮点数但我需要整数。(“中式英语”也许可以写成“This is float type but need int type”:))

要使两个浮点数相加,你需要用操作符+. (注意加号后面的点号。)

OCaml不会自动把int提升到float,因此下面这个表达式也是错误的:

# 1 +. 2.5;;
Error: This expression has type int but an expression was expected of type float

这里OCaml会指出第一个参数类型是错误的。

如果我们确实需要让一个整数与一个浮点数相加该怎么办呢?(假设它们存储于变量if)。在OCaml中我们需要显式转换:

float_of_int i +. f;;

float_of_int函数输入整数为参数返回一个浮点数。与它相似,我们有一系列这样的函数: int_of_float, char_of_int, int_of_char, string_of_int 等等,它们的作用大都象字面意思一样。

因为从int转换到float是个特别常用的操作,float_of_int函数有个简短的别名。上面的例子可以简单地写作:

float i +. f;;

(注意和C不一样的是,类型和函数同名在OCaml中是完全合法的)

隐式转换和显式转换哪个更好?

你也许会认为显式转换减少代码美观,增加录入时间。这有一定道理,但至少有三个理由支持显式转换。首先,显 式转换帮助OCaml来实现类型推导(详后),类型推导节省的时间足以抵消多输入字符增加的时间。其次,如果你有C编程调试的经验,你应该有体会(1)隐 式转换会造成难以发现的错误,(2)找出哪里发生了隐式转换经常占用了很大一部分调试时间。第三,有些转换(特别是整型和浮点型的互换)其实是很占资源的 操作。把它们隐藏起来并没有好处。

普通函数和递归函数

和源于C的语言不同的是,OCaml中的函数一般不是递归的,除非你用let rec代替let来定义递归函数。下面是一个递归函数的例子:

# let rec range a b =
    if a > b then []
    else a :: range (a+1) b;;
val range : int -> int -> int list = <fun>

注意这里range调用它自身。

letlet rec的唯一区别是函数名的定义域。如果以上函数是用 let定义的,那么调用range会试图寻找一个已经存在的(以前定义)的叫 range的函数,而不是现在正在被定义的函数。 let允许你使用变量本身进行重新定义。例如:

# let positive_sum a b = 
    let a = max a 0
    and b = max b 0 in
    a + b;;
val positive_sum : int -> int -> int = <fun>

这里let中的ab重新定义了所绑定的值,因此a + b所看到的值是let所绑定的新值而不是函数的传参。在某些情况,程序员往往更倾向于这种风格,而不是let a_pos = max a 0,因为这样做可以使得只有最新的绑定可见。

译注:与其他函数式编程语言类似,OCaml的“变量”是不可变的,所以与其说是变量,更应该理解成是一种绑定关系。而ML衍生的语言是可以重新绑定的,而原来的值依然存在,只不过是被新绑定隐藏掉了,并不是一般面向过程语言中的变量赋值。另外,有些函数式语言是不能重新绑定(或者定义)的,如Erlang。

letlet rec定义的函数并没有性能上的差别,所以如果你愿意你可以一直用let rec来定义如C中那样的函数。

函数的类型

类型推导的存在使得我们几乎不需要显式的写出函数的类型。但OCaml经常显示出它认为的函数类型,因此了解有关语法还是需要的。对于一个函数f,其参数类型为arg1arg2,... argn,其返回值类型为 rettype,编译器会显示

f : arg1 -> arg2 -> ... -> argn -> rettype

箭头符号用在这里会显得很奇怪,但是当我们以后介绍了所谓"currying(科里化,维基有详述)"后,你就会明白为什么用它。现在我只是给出几个示例。

我们定义的repeated函数需要一个字符串和一个整数为参数,返回一个字符串。

repeated : string -> int -> string

我们定义的average函数输入两个浮点数并返回一个浮点数:

average : float -> float -> float

OCaml标准的类型转换函数int_of_char

int_of_char : char -> int

如果一个函数没有返回值(如C或java中的void),那我们写成它返回unit类型。比如:OCaml中的等价于fputc的函数:

output_char : out_channel -> char -> unit

多态函数

现在我们来看一个比较奇怪的问题。如果一个函数的参数可以是任何类型怎么办?下面就是一个奇怪的函数,它接受任何参数但总是返回3。

let give_me_a_three x = 3;;

那么这样的函数的类型是什么呢?OCaml使用一个特殊的占位符来表示“任意类型”,这就是一个单引号后加一个字母。 上述函数的类型可以写作:

give_me_a_three : 'a -> int

这里'a表示任意类型。我们可以如give_me_a_three "foo"这样也可以象give_me_a_three 2.0这样来调用这个函数。它们都是OCaml合法的表达式。

这里我们还看不出多态函数的用处,而其实它们很有用也很普遍,我们将稍后详述。(提示:多态性有些象C++中的templates或者Java1.5中的generics)

类型推导

本教程的主题是函数语言有很多非常酷的特性,OCaml作为函数式语言具有其所有的特性,因此它是程序员的很实用的语言。但是奇怪的是,大多数特性却与函数式编程无关。事实上在还没有介绍函数式编程为什么叫函数式之前,我已经涉及了第一个酷特性,那就是类型推导。

简单地说:你不需要声明函数和变量的类型,因为OCaml自己会知道。

而且OCaml会一直检查所有的类型匹配(甚至在不同的文件之间)。

但OCaml同时也是一个实用的语言。所以它的类型系统存在后门使你在一些特殊场合可以避开这些检查。只有资深专家才有可能需要避开类型检查。

我们再次来看average函数,我们在OCaml的toplevel中输入它,

# let average a b =
    (a +. b) /. 2.0;;
val average : float -> float -> float = <fun>

神奇吧?OCaml自己判断出了这个函数需要两个浮点数参数和返回一个浮点数。

它是如何做到的呢?首先它看ab在哪里使用,这里是在表达式 (a +. b)中。这里+.本身是一个需要两个浮点数参数的函数,所以通过简单推导,ab两个都是浮点数。

其次,函数/.返回浮点类型,所以average函数同样返回浮点类型。因此,结论就是average的类型如下:

average : float -> float -> float

类型推导不仅适用于短程序,也适用于大规模的程序。它是一个主要的节省时间的特性,因为它消除了一系列在其他语言中常见的造成segfault,NullPointerExceptionClassCastException的错误(或者是如Perl中,一些很重要但是经常被忽略的运行时警告)。