OCaml学习——《Real-World-Ocaml》Chap1

Chap1 导览旅行

配好了 Emacs 的环境,敲一下样例。

函数类型和类型推断

初次见面,函数和类型推断。

1
2
3
4
5
let square x = x*x;;

square 2;;

square ( square 2);;

输出结果

1
2
3
4
5
6
# let square x = x*x;;
val square : int -> int = <fun>
# square 2;;
- : int = 4
# square ( square 2);;
- : int = 16

第一使用模块,Float.of_int指示of_int函数包含在Float模块中。模块名总是以一个大写字母开头。

1
2
let ratio x y =
Float.of_int x /. Float.of_int y;;

初涉推断类型与推断泛型类型,理解一下类型推断的过程,比如 OCaml 要求 if 语句分支必须具有相同类型,前者从 0 推断 first 类型,后者没有直接的线索,引入了类型变量(type ariable)。

1
2
3
4
5
6
7
8
(* 类型推断  *)
let sum_if_true test first second =
(if test first then first else 0)+(if test second then second else 0);;


(* 推断泛型类型 *)
let first_if_true test x y =
if test x then x else y;;

元组、列表、选项和模式匹配

元组,顶层环境显示为int*string,类型对集合对应两集合的笛卡尔乘积。模式匹配可抽取元组中的分量。

1
2
3
let a_tuple = (3,"tree");;
let b_tuple = (3,"four",5.);;
let x,y,z = b_tuple;;
1
2
3
4
5
6
7
8
# let a_tuple = (3,"tree");;
val a_tuple : int * string = (3, "tree")
# let b_tuple = (3,"four",5.);;
val b_tuple : int * string * float = (3, "four", 5.)
# let x,y,z = b_tuple;;
val x : int = 3
val y : string = "four"
val z : float = 5.

列表可以保存任意数目相同类型的元素。注意使用分号,不然会当作是一个元组作元素的单元素列表,如下:

1
2
let languages = ["OCaml","Perl","C"];;
let languages = ["OCaml";"Perl";"C"];;
1
2
3
4
# let languages = ["OCaml","Perl","C"];;
val languages : (string * string * string) list = [("OCaml", "Perl", "C")]
# let languages = ["OCaml";"Perl";"C"];;
val languages : string list = ["OCaml"; "Perl"; "C"]

除了使用括号构造列表,还可以使用操作符“::”在列表前面增加元素,括号记法实际上“::”的语法糖(syntactic sugar)。“::”左结合,“[]”表示空列表。“::”操作符只能在最前面增加一个元素,“@”操作符可以用来连接两个列表。

1
2
3
4
5
6
"test"::languages;;
"test"::[];;
(* 下面的声明是等价的 *)
[1;2;3];;
1::(2::(3::[]));;
1::2::3::[];;
1
2
3
4
5
6
7
8
9
10
# "test"::languages;;
- : string list = ["test"; "OCaml"; "Perl"; "C"]
# "test"::[];;
- : string list = ["test"]
# [1;2;3];;
- : int list = [1; 2; 3]
# 1::(2::(3::[]));;
- : int list = [1; 2; 3]
# 1::2::3::[];;
- : int list = [1; 2; 3]

列表的模式匹配,注意模式不完备问题,使用 match 语句。

1
2
3
4
5
6
7
8
9
(* 列表的模式匹配  *)
let my_favorite_language (my_favorite :: the_rest) =
my_favorite;;
let my_favorite_language my_favorite =
match my_favorite with
| first :: the_rest -> first
| [] -> "None";;
my_favorite_language [];;
my_favorite_language ["English";"Math"];;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# let my_favorite_language (my_favorite :: the_rest) =
my_favorite;;
Characters 25-66:
.........................(my_favorite :: the_rest) =
my_favorite..
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val my_favorite_language : 'a list -> 'a = <fun>
# let my_favorite_language my_favorite =
match my_favorite with
| first :: the_rest -> first
| [] -> "None";;
val my_favorite_language : string list -> string = <fun>
# my_favorite_language [];;
- : string = "None"
# my_favorite_language ["English";"Math"];;
- : string = "English"

递归列表函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(* 递归列表函数  *)
let rec sum l =
match l with
| [] -> 0 (* 基本情况 *)
| hd::tl -> hd + sum tl;; (* 归纳情况 *)

let rec sum_length l =
match l with
| [] -> 0 (* 基本情况 *)
| hd :: tl -> String.length hd + sum_length tl;; (* 归纳情况 *)

sum [5;4;3;2];;
sum [];;
sum_length ["What";"tes";"ab";"1"];;
sum_length [];;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# let rec sum l =
match l with
| [] -> 0 (* 基本情况 *)
| hd::tl -> hd + sum tl;;
val sum : int list -> int = <fun>
# let rec sum_length l =
match l with
| [] -> 0 (* 基本情况 *)
| hd :: tl -> String.length hd + sum_length tl;;
val sum_length : Core.Std.String.t list -> int = <fun>
# sum [5;4;3;2];;
- : int = 14
# sum [];;
- : int = 0
# sum_length ["What";"tes";"ab";"1"];;
- : int = 10
# sum_length [];;
- : int = 0
# sum_length [];;
- : int = 0

选项(option)结构,表示一个值可能有也可能没有。选项在 OCaml 中很重要,在 OCaml 中没有类似NullPointerException的概念。这一点与大多数其他语言都有所不同。

记录和变体

记录和变体,和其他语言的自定义类型有些相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# type point2d = { x: float ; y: float };;
type point2d = { x : float; y : float; }
(* 非字符双关 *)
let magnitude { x=x_pos;y=y_pos} =
sqrt (x_pos **2. +. y_pos **2.);;
(* 字符双关写法 *)
let magnitude {x;y} =
sqrt (x**2. +. y**2.);;

let distance v1 v2 =
magnitude {x = v1.x -. v2.x ; y = v1.y -. v2.y};;

let p_1 = {x=3.;y=4.}
in
let p_2 = {x=5.2;y=4.4}
in
distance p_1 p_2 ;;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# let magnitude { x=x_pos;y=y_pos} =
sqrt (x_pos **2. +. y_pos **2.);;
val magnitude : point2d -> float = <fun>
# let magnitude {x;y} =
sqrt (x**2. +. y**2.);;
val magnitude : point2d -> float = <fun>
# let distance v1 v2 =
magnitude {x = v1.x -. v2.x ; y = v1.y -. v2.y};;
val distance : point2d -> point2d -> float = <fun>
# let p_1 = {x=3.;y=4.}
in
let p_2 = {x=5.2;y=4.4}
in
distance p_1 p_2;;
- : float = 2.23606797749979

可以将新定义的类型作为分量包含在更大的类型中。以及构成包含多个对象的场景的描述,变体(ariant)类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type circle_desc = {center: point2d; radius: float};;
type rect_desc = {lower_left: point2d; width: float; height: float};;
type segment_desc = {endpoint1: point2d; endpoint2: point2d};;
type scene_element =
| Circle of circle_desc
| Rect of rect_desc
| Segment of segment_desc
;;

(* match 恰与不同场景匹配 *)
let is_inside_scene_element point scene_element =
match scene_element with
| Circle {center;radius} -> distance center point < radius (* 注意这是个比较,最后返回的是bool *)
| Rect {lower_left; width; height} ->
point.x > lower_left.x && point.y > lower_left.y &&
point.x < lower_left.x +. width && point.y < lower_left.y +. height
| Segment {endpoint1;endpoint2} -> false
;;

命令式编程

到目前为止所写都是纯代码或函数式代码。几乎所有数据结构都是不可变的,这与命令式编程完全不同,命令式编程中,需要建立指令序列通过修改程序的状态来完成计算。

数组与可变记录字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let numbers = [|1;2;3|];;
numbers.(2) <-4;; (* .(1)语法用于指示数组的一个元素,<-语法表示修改。从0计数 *)
numbers;;

(* 把一些字段显式声明为可变字段,下面例子用于存储对一个数字集合的统计汇总 *)

type running_sum =
{
mutable sum: float;
mutable sum_sq: float;
mutable samples: int;
}
;;

let mean rsum = rsum.sum /. float rsum.samples;;
let stdev rsum =
sqrt (rsum.sum_sq /. float rsum.samples -. (rsum.sum /. float rsum.samples)**2.)
;;

还有ref,以及循环for,while的用法。

一个完整程序

完整程序编译运行,

1
2
corebuild chap1-sum.native
./sum.native

输入数求和,C-d结束输入。