Ocaml学习

网上关于Ocaml的资料比较少,可见它是一门偏小众的语言。不过在DSL和程序分析方面,Ocaml是十分强大的。

函数的定义和调用

Ocaml中,定义函数的语法很简单。这个函数是输入两个浮点数后计算它们的平均值。

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

在C当中,如果要定义一个相同的函数,其定义是这样的:

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

可以看到,OCaml没有定义a和b的类型,而且也没有所谓的return,而且写的是2.0,没有用隐式转换。这其实是由Ocaml语言的特性决定的:

  • Ocaml是强静态类型的语言
  • OCaml使用类型推倒,不需要注明类型
  • OCaml不做任何隐式转换(所以要写浮点数就必须是2.0)
  • OCaml不允许重载,+.表示两个浮点数相加,也就是说操作符是和类型相关的
  • OCaml的返回值是最后的表达式,不需要return

和大多数基于C的语言不同,OCaml的函数调用,是没有括号的。例如定义了一个函数repeated,它的参数是一个字符串s和一个数n,那么它的调用形式会是:

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

可以看到既没有括号,也没有都好。不过reoeated ("hello", 3)也是合法的,只不过它的参数是一个含两个元素的对(pair)。

基本类型、转换与推倒

类型 范围
int 32bit:31位;64bit:63位
float 双精度,类似于C中的double
bool true/flase
char 8bit字符
string 字符串
unit 写作(),类似void

这里,Ocaml内部使用了int中的一位来自动管理内存(垃圾收集),因此会少一位。 前面也提到,OCaml是没有隐式类型转换的。因此

1 + 2.5;;
1 +. 2.5;;

在OCaml中是会报错的。但是如果一定要让一个整数和浮点数相加,就必须显示的进行转换,比如:

float_of_int i +. f;;
float i +. f;;

许多情况下,不需要声明函数的变量和类型,因为Ocaml自己会知道,它会一直检查所有的类型匹配。比如前面的average函数,就能够自给判断出这个函数需要两个浮点数参数和返回一个浮点数。

函数的递归和类型

和基于C的语言不同之处在于,OCaml中,函数一般不是递归的,除非用let rec代替let定义递归函数。这是一个递归函数的例子:

let rec range a b =
	if a > b then []
	else a :: range (a+1) b

let和let rec的唯一区别,就是函数的定义域。举个例子,如果用let定义range,那么range会去找一个已经定义好的函数,而不是它自身。不过在性能上,letlet rec并没有太大的差异。所以即使全部用let rec来定义也可以。 而OCaml的类型推倒,也使得几乎不用显式的写出函数的类型。不过Ocaml经常以这样的实行显示参数和返回值的类型:

f:arg1 -> arg2 -> ... -> argn -> rettype
f: 'a->int	(*单引号表示人意类型*)

表达式

在Ocaml当中,局部变量/全局变量其实都是一个表达式。例如,局部表达式有:

let average a b =
	let sum = a +. b in
	sum /. 2.0;;

标准短语let name = expression in是用来定义一个命名的局部表达式的。name在这个函数当中,就可以代替expression,直到一个;;结束这个代码块。这里把let ... in视为一个整体。和C中不一样,OCaml中name只是expression的一个别名,我们是不能给name赋值或者改值的。
而全局表达式,也可以像定义局部变量一样定义全局名,但这些也不是真正的变量,而只是缩略名。

let html =
let content = read_whole_file file in
  GHtml.html_from_string content
  ;;

let menu_bold () =
  match bold_button#active with
  | true -> html#set_font_style ~enable:[`BOLD] ()
  | false -> html#set_font_style ~disable:[`BOLD] ()
  ;;

let main () =
  (* code omitted *)
  factory#add_item "Cut" ~key:_X ~callback: html#cut
  ;;

这里,html实际上是一个“小部件”,没有指针去保存它的地址,也不能赋值,而是在之后的两个函数中被引用。

Let-绑定

绑定,let ...,能够在OCaml中,实现真正的变量。在OCaml中,引用使用关键字ref来进行定义。例如,

let my_ref = ref 0;;(*引用保存着一个整数0*)
myref := 100(*引用被赋值为100*)

:=用来给引用赋值,而用来取出引用的值。以下是一个C和OCaml的比较

OCaml                   C/C++

let my_ref = ref 0;;    int a = 0; int *my_ptr = &a;
my_ref := 100;;         *my_ptr = 100;
!my_ref                 *my_ptr

嵌套函数

与C语言不同的是,OCaml是可以使用嵌套函数的。

  let read_whole_channel chan =
  let buf = Buffer.create 4096 in
  let rec loop () =
    let newline = input_line chan in
    Buffer.add_string buf newline;
    Buffer.add_char buf '\n';
    loop ()
  in
  try
    loop ()
  with
    End_of_file -> Buffer.contents buf;;

这里,loop是只有一个嵌套函数,在read_whole_channel中,是可以调用loop()的,但它在read_whole_channel当中并没有定义,嵌套函数可以使用主函数当中的变量,它的格式和局部命名表达式是一致的。

模块和OPEN

OCaml也提供了很多模块,包括画图、数据结构、处理大数等等。这些库位于usr/lib/ocaml/VERSION。例如一个简单的模块Graphics,如果想使用其中的函数,有两种方法。第一种是在开头声明open Graphics,第二种是在函数调用之前加上前缀,例如Graphics.open_graph
如果想用 Graphics当中的符号,也可以通过重命名的方式,简化前缀。

module Gr = Graphics;;

这个技巧在模块嵌套时十分有用。

;;还是;,或者什么都不用?

在OCaml中,有时候会使用;;,有时候会使用;,有时候却什么都不用,这就让初学者很容易迷惑。这里,OCaml实际上定义了一系列的规则。
#1 必须使用;;在代码的最顶端,来分隔不同语句(不同代码段之间的分隔),并且不要在函数定义或其他语句中使用
#2 可以在某些时候省略掉;;,包括letopentype之前,文件的最后,以及OCaml能自动判断的地方
#3 let ... in是一条单独道语句,不能在后面加单独的;
#4 所有代码块中其他语句后面,跟上一个单独的;,最后一个例外

看到这些规则,我依然没有完全理解这三者的用法。我想,只有实际接触过Ocaml代码,才能逐渐体会到其中的精髓吧。

模块

OCaml把每一段代码,都包装成一个模块。例如两个文件amodule.mlbmodule.ml都会定义一个模块,分别为Amodule和Bmodule。 通常模块是一个个编译的,比如

ocamlopt -c amodule.ml
ocamlopt -c bmodule.ml
ocamlopt -o hello amodule.cmx bmodule.cmx

那么访问模块中的内容可以使用open,也可以使用module.func这样的方式。
通常模块会定义为 struct...end的形式,这样能够形成一个有效的闭包,防止命名的重复等,它需要和一个module关键字绑定。比如:

module PrioQueue = 
	struct
	...
	end;;

接口、签名

通常模块中的所有定义,都可以从外部进行访问。但实际中,模块只应该提供一系列接口,隐藏一些内容,这也是面向对象语言中所提倡的。模块是定义在.ml文件中的,而相应的接口,则是从.mli文件中得到的。它包含了一个带有类型的值的表。例如,对于一个模块来说,它的接口可以这样定义:

(*模块定义*)
let message = "Hello"
let hello() = print_endline message

(*接口定义*)
val hello : uint -> unit

这样,接口的定义就隐藏了message。这里,.mli文件是在.ml文件之前编译的。.mliocamlc来编译,而.ml则是用ocamlopt来编译的。.mli文件就是所说的“签名”。

ocamlc -c amodule.mli
ocamlopt -c amodule.ml

类型

值可以通过把它们的名字和类型,放到.mli文件的方式来导出。

val hello : unit -> unit

但模块经常定义新的类型。例如,

type date = { day : int; month : int; year : int }

这里其实有几种.mli文件的写法,例如,包括:

  • 完全忽略类型
  • 把类型定义拷贝到签名
  • 把类型抽象,只给出名字type date
  • 把域做成只读的:type date = private{...}

如果采用第三种方式,那么模块的用户就只能操作date对象,使用模块提供的函数去间接进行访问。

子模块

一个给定的模块,可以在文件中显示的定义,成为当前模块的字模块。通过约束一个给定自模块的接口,是能够达到和写一对.mli/.ml文件一样的效果的。例如:

module type Hello_type = sig
val hello : unit -> unit
end

module Hello : Hello_type = struct
...
end

仿函数(函子)

OCaml中的仿函数,定义与其他语言不太一样,它是用另一个模块,来参数化的模块。它允许传入一个类型作为参数,但这在OCaml中直接做是不可能的。个人理解,这里的函子和C++中的STL比较类似,它接受不同类型的输入作为初始化。事实上在OCaml中,map和set模块都是要通过函子来使用的。
例如,标准库定义的 Set模块,就提供了一个Make函子。假如要使用不同类型的集合,可以这样这样利用函子:

# module Int_set = Set.Make (struct type t = int let compare = compare end)
# module String_set = Set.Make (String);;

至于函子的定义,则是这样:

module F(X : X_type) = struct
	...
end

X是作为参数被传递的模块,而X_type是它的签名,这种写法是强制。

module F(X:X_type) : Y_type = 
struct
	...
end

这种写法对于返回模块的签名,也能够进行约束。函子的操作也是比较难理解的,多使用set/map,并且阅读这两个库中的源码,是能够帮助理解和记忆的。

模式匹配

OCaml能够把数据结构分开,并对其做模式匹配。这里举一个例子,表示一个数学表达式n * (x + y),并且分解公因式为n * x + n * y
首先定义一个表达式类型:

# type expr =
| Plus of expr * expr        (* means a + b *)
| Minus of expr * expr       (* means a - b *)
| Times of expr * expr       (* means a * b *)
| Divide of expr * expr      (* means a / b *)
| Value of string            (* "x", "y", "n", etc. *);;

那么,对于一个表达式,用模式匹配的方式,可以将其打印成对应的数学表达式:

# let rec to_string e =
match e with
| Plus (left, right) ->
   "(" ^ to_string left ^ " + " ^ to_string right ^ ")"
| Minus (left, right) ->
   "(" ^ to_string left ^ " - " ^ to_string right ^ ")"
| Times (left, right) ->
   "(" ^ to_string left ^ " * " ^ to_string right ^ ")"
| Divide (left, right) ->
   "(" ^ to_string left ^ " / " ^ to_string right ^ ")"
| Value v -> v;;

val to_string : expr -> string = <fun>
# let print_expr e =
	print_endline (to_string e);;
val print_expr : expr -> unit = <fun>

这样,使用print_expr,就能够把一个表达式打印成一个数学表达式。那么,模式匹配的通用形式是:

match value with
| pattern    ->  result
| pattern    ->  result
  ...

或者对条件进行进一步的约束

match value with
| pattern  [ when condition ] ->  result
| pattern  [ when condition ] ->  result
  ...

注意,这里还有一种特殊的模式匹配,| _,它用来匹配剩下的任意情况。

奇奇怪怪的操作符

OCaml中,还有许多有趣的操作符和表达式。在SO上,我也看到了类似的提问:

let m = PairsMap.(empty |> add (0,1) "hello" |> add (1,0) "world") 

这里有两个问题。第一个,module.(e)是啥意思?它其实等价于let open Module in e,它相当于一种简写的形式,同样是把module引入当前模块的方式。
第二个 |>表达式是什么意思?其实它是Pervasives中定义的一个操作符,其定义为let (|>) x f = f x。它被称为"reverse application function"(我不知道应该如何翻译),但它的作用,是把连续的调用去有效的串联起来(可以把函数放在参数之后,从而保证一个调用顺序,有一点类似管道的意思)。如果不使用|>符号,那么就必须写成:

let m = PairsMap.(add(1,0) "world" (add(0,1) "hello" empty))

在Uroboros当中,还看到有一个奇怪的操作符,那便是@,从manual上来看,这个操作符的意思是“串联List”。有这样的例子:

# List.append [1;2;3] [4;5;6];;
- : int list = [1; 2; 3; 4; 5; 6]
# [1;2;3] @ [4;5;6];;
- : int list = [1; 2; 3; 4; 5; 6]



本文链接: http://home.meng.uno/articles/b303bc00/ 欢迎转载!

© 2018.02.08 - 2020.06.02 Mengmeng Kuang  保留所有权利!

UV : | PV :

:D 获取中...

Creative Commons License