首页 > 技术文章 > OCaml入门(5)

gyhang 2015-09-06 12:34 原文

匿名函数

函数式语言中,函数是一等公民,它应该可以像其它类型(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>   

含义与前边的定义完全相同。

 

推荐阅读