匿名函数
函数式语言中,函数是一等公民,它应该可以像其它类型(int,float,string等)一样,能支持“字面量”,这就是匿名函数。
utop # (fun x -> x + 1);; - : int -> int = <fun> utop # (fun x -> x + 1) 7;; - : int = 8
可以看到,匿名函数可以代替函数的位置来使用。
有些函数需要的参数是另一个函数,当然也可以用匿名函数传入。比如:
utop # List.map ~f:(fun x->x+1) [1;2;3];; - : int list = [2; 3; 4]
不要太在意: ~f: 这样的语法,这只是具名函数调用的用法。大多数语言中,函数的调用都是按位置来匹配参数的。
这样做的缺点是容易忘记哪个参数在哪个位置。Ocaml当然也支持按位置传参调用函数的方式,但同时鼓励按名字传参来调用函数,这样的程序更具可读性。
(你可能会觉得这多别扭啊,暂且忍受,人们往往都是习惯的奴隶)
匿名函数当然能绑定到一个符号,这样就成了具名函数:
utop # let plusone = fun x -> x + 1;; val plusone : int -> int = <fun> utop # plusone 5;; - : int = 6
这种定义如此普遍,Ocaml提供了语法糖来简化:
utop # let plusone x = x + 1;; val plusone : int -> int = <fun>
这与上一个定义是完全一样的,只是语法偷懒了而已。
匿名函数也可以有多个参数:
utop # let diff x y = abs (x - y);; val diff : int -> int -> int = <fun> utop # diff 5 8;; - : int = 3
你可能会很奇怪这个函数的类型签名: int -> int -> int
在我们的想象中,应该是:(int, int)-> int 才更合理啊。
这正是函数式语言更数学化的特征之一。在数学上,严格地说,每个函数都只能传入一个参数,并且返回一个参数。
如果有一个函数看起来传入两个参数,它只不过是传入一个参数又返回了另一个函数的函数。
也就是说,本质上,上边的函数定义等价于:
utop # let diff = fun x -> (fun y -> abs (x - y));; val diff : int -> int -> int = <fun>
这是个脑力体操,仔细想想吧。
函数式语言的威力恰在于:函数可以作为参数,函数可以作为返回值。
按照这个思路,我们顺理成章地可以这样写:
utop # let diff3 = diff 3;; val diff3 : int -> int = <fun> utop # diff3 8;; - : int = 5
对于多参的函数,只提供部分参数的做法很普遍,数学上叫做:偏参函数。
函数的递归
函数式语言不鼓励(甚至是不允许)使用变量 + 循环的编程模式,比起循环来,它更青睐于“递归”。
递归就是一函数直接或间接地调用自己。
utop # let rec len lst = match lst with | [] -> 0 | h::tail -> 1 + len tail;; val len : 'a list -> int = <fun> utop # len [1;2;3;4];; - : int = 4 utop # len [];; - : int = 0
如果一函数是递归的,Ocaml要求必须明确地用 rec来修饰。
rec是recursive的缩写。
如果间接递归就需要一次把多个函数都定义出来,比如: is_even 来判断是否为偶数。这里只是为了示范概念,当然仅仅为这个小功能没必要这样大费周章。
utop # let rec is_even x = if x=0 then true else is_odd (x-1) and is_odd x = if x=0 then false else is_even (x-1);; val is_even : int -> bool = <fun> val is_odd : int -> bool = <fun> utop # is_even 7;; - : bool = false utop # is_even 8;; - : bool = true
前缀与中缀
一般的函数调用都是前缀格式: 函数 参数 参数 ...
有的时候,两个参数时,中缀更符合习惯。比如: 1 + 2, 5 mod 3
Ocaml中,函数与运算符实在同样的东西,都是: function
中缀运算符如果加上括号,就回到了前缀的风格:
utop # (+) 5 8;; - : int = 13 utop # (mod) 10 3;; - : int = 1 utop # List.map ~f:((+) 3) [1;2;3;4];; - : int list = [4; 5; 6; 7]
自己定义中缀运算符也可以,必须从如下符号集合中组合:
! $ % & * + - . / : < = > ? @ ^ | ~
比如:
utop # let (+!) (x1,y1) (x2,y2) = x1+x2, y1+y2;; val ( +! ) : int * int -> int * int -> int * int = <fun> utop # (1,2) +! (10,20);; - : int * int = (11, 22)
使用含有 * 的运算符需要格外小心,因为 (* 。。。。 *) 表示注释语句。
utop # ( * ) 3 5;; - : int = 15
这里括号与星号之间的空格是必须的!
Function
let ff x = match x with ... | ... | ...
这种模式如此普遍,OCaml提供了进一步的简化语法:
utop # let len = function | [] -> 0 | h::t -> 1 + len t;; val len : 'a list -> int = <fun> utop # len [1;2];; - : int = 2
标签参数
多参数函数可以使用标签参数来增加可读性和灵活性。
下面的函数求坡度。
utop # let po ~path ~height = let pi = atan 1.0 *. 4. in (asin (height /. path)) *. 180.0 /. pi;; val po : path:float -> height:float -> float = <fun> utop # po ~path:10. ~height:1.;; - : float = 5.73917047727
既然参数有名字,顺序就无关紧要了。
utop # po ~height:1. ~path:10.;; - : float = 5.73917047727
可选参数
可选参数就是可能出现,也可能省略的参数。在OCaml类型中表现为Option类型。
utop # let concat ?sep x y = let s = match sep with None->"" | Some x -> x in x ^ s ^ y;; val concat : ?sep:string -> string -> string -> string = <fun> utop # concat "dog" "cat";; - : string = "dogcat" utop # concat "dog" "cat" ~sep:",";; - : string = "dog,cat"
对于Option类型的解构太常用了,所以Ocaml进一步提供了简便语法:
utop # let concat ?(sep="") x y = x ^ sep ^ y;; val concat : ?sep:string -> string -> string -> string = <fun>
含义与前边的定义完全相同。