OCaml 教程 / 函数定义与调用
函数定义与调用
概述
函数是 OCaml 编程的核心。OCaml 是函数式语言,函数是一等公民(first-class citizen),可以作为参数传递、作为返回值返回、存储在数据结构中。
fun 关键字与基本函数定义
(* 最简单的函数 *)
let greet name = "Hello, " ^ name
(* 带类型注解 *)
let add (a : int) (b : int) : int = a + b
(* 函数调用 — 不需要括号 *)
let result = add 3 5 (* => 8 *)
let msg = greet "World" (* => "Hello, World" *)
💡 提示:OCaml 函数调用不使用括号包裹参数(除非参数是复杂表达式)。f a b 而不是 f(a, b)。
参数与返回类型
类型推导
(* 编译器自动推导类型 *)
let double x = x * 2
(* 类型: int -> int *)
let greet name = "Hello, " ^ name
(* 类型: string -> string *)
let is_even x = x mod 2 = 0
(* 类型: int -> bool *)
显式类型注解
(* 参数注解 *)
let add (x : int) (y : int) = x + y
(* 返回值注解 *)
let add (x : int) (y : int) : int = x + y
(* 完整函数签名 — 使用函数类型语法 *)
let add : int -> int -> int = fun x y -> x + y
⚠️ 注意:在 OCaml 中,多参数函数实际上是**柯里化(Currying)**的——int -> int -> int 等价于 int -> (int -> int),即接受一个 int 并返回一个 int -> int 的函数。
多参数函数与柯里化
OCaml 中所有多参数函数默认是柯里化的:
(* 多参数函数 *)
let add a b = a + b
(* 类型: int -> int -> int *)
(* 部分应用(Partial Application) *)
let add_5 = add 5 (* 类型: int -> int *)
let result = add_5 3 (* => 8 *)
(* 实际应用:创建专用函数 *)
let multiply a b = a * b
let double = multiply 2
let triple = multiply 3
let _ = double 7 (* => 14 *)
let _ = triple 7 (* => 21 *)
元组参数 vs 柯里化参数
(* 柯里化风格 — 默认 *)
let add_curried a b = a + b
(* 类型: int -> int -> int *)
(* 元组风格 — 接受一个元组参数 *)
let add_uncurried (a, b) = a + b
(* 类型: int * int -> int *)
(* 调用方式不同 *)
let r1 = add_curried 3 5 (* 用空格分隔 *)
let r2 = add_uncurried (3, 5) (* 传入元组 *)
(* 部分应用只对柯里化函数有效 *)
let add3 = add_curried 3 (* ✅ 可以 *)
(* let add3 = add_uncurried 3 ❌ 编译错误 *)
| 特性 | 柯里化 | 元组参数 |
|---|---|---|
| 类型 | int -> int -> int | int * int -> int |
| 调用 | f a b | f (a, b) |
| 部分应用 | ✅ 支持 | ❌ 不支持 |
| 与高阶函数配合 | 自然 | 需要适配 |
转换函数
(* 柯里化 -> 元组 *)
let curry f a b = f (a, b)
(* 元组 -> 柯里化 *)
let uncurry f (a, b) = f a b
(* 使用示例 *)
let add_tupled (a, b) = a + b
let add_curried = curry add_tupled
let _ = add_curried 3 5 (* => 8 *)
递归与 let rec
在 OCaml 中定义递归函数需要使用 let rec:
(* 阶乘 *)
let rec factorial n =
if n <= 1 then 1
else n * factorial (n - 1)
let _ = factorial 5 (* => 120 *)
(* 斐波那契数列 *)
let rec fib n =
match n with
| 0 -> 0
| 1 -> 1
| n -> fib (n - 1) + fib (n - 2)
let _ = fib 10 (* => 55 *)
(* 列表长度 *)
let rec length lst =
match lst with
| [] -> 0
| _ :: rest -> 1 + length rest
let _ = length [1; 2; 3; 4; 5] (* => 5 *)
⚠️ 注意:普通 let 绑定的函数不能引用自身。如果需要递归,必须使用 let rec。
相互递归(Mutual Recursion)
使用 let rec ... and ... 定义相互递归的函数:
(* 判断奇偶数 — 经典的相互递归示例 *)
let rec is_even n =
if n = 0 then true
else is_odd (n - 1)
and is_odd n =
if n = 0 then false
else is_even (n - 1)
let _ = is_even 4 (* => true *)
let _ = is_odd 5 (* => true *)
(* 解析器组合器中常见的相互递归 *)
let rec parse_expr tokens =
(* 解析表达式,可能调用 parse_term *)
parse_term tokens
and parse_term tokens =
(* 解析项,可能调用 parse_factor *)
parse_factor tokens
and parse_factor tokens =
(* 解析因子 *)
tokens (* 简化示例 *)
💡 提示:相互递归在编写解析器和状态机时非常有用。
匿名函数(Lambda)
(* 使用 fun 关键字定义匿名函数 *)
let square = fun x -> x * x
(* 等价于 *)
let square x = x * x
(* 在高阶函数中使用 *)
let doubled = List.map (fun x -> x * 2) [1; 2; 3]
(* => [2; 4; 6] *)
(* 多参数匿名函数 *)
let add = fun a b -> a + b
(* 嵌套匿名函数 *)
let f = fun x -> fun y -> x + y
(* 等价于 *)
let f x y = x + y
(* 使用 function 关键字 — 仅一个参数,可直接模式匹配 *)
let describe = function
| 0 -> "零"
| 1 -> "一"
| _ -> "其他"
(* 等价于 *)
let describe x = match x with
| 0 -> "零"
| 1 -> "一"
| _ -> "其他"
💡 提示:fun 可以有多个参数(fun a b -> ...),而 function 只接受一个参数但自带模式匹配。
命名参数(Labeled Arguments)
OCaml 支持命名参数,让函数调用更清晰:
(* 定义带命名参数的函数 *)
let ~name ~age =
Printf.sprintf "%s, %d years old" name age
(* 调用时指定参数名 *)
let msg = ~name:"Alice" ~age:30
(* => "Alice, 30 years old" *)
(* 参数顺序可以调换 *)
let msg' = ~age:30 ~name:"Alice"
(* => "Alice, 30 years old" *)
(* 混合位置参数和命名参数 *)
let create_user username ~email ~age =
Printf.sprintf "User: %s, Email: %s, Age: %d" username email age
let user = create_user "alice" ~email:"[email protected]" ~age:30
⚠️ 注意:命名参数的语法是 ~name(定义时)和 ~name:value(调用时)。在类型注解中,命名参数写作 name:string -> 。
标签擦除
当命名参数被部分应用时,标签会被"擦除":
let f ~x ~y = x + y
(* 类型: x:int -> y:int -> int *)
let g = f ~x:3
(* g 的类型: y:int -> int — 标签 ~y 保留 *)
let h = f ~x:3 ~y:5
(* h 的类型: int — 标签被擦除 *)
可选参数与默认值
OCaml 的命名参数可以是可选的,当不提供时使用默认值:
(* 带默认值的可选参数 *)
let greet ?(prefix = "Hello") name =
prefix ^ ", " ^ name ^ "!"
let _ = greet "Alice" (* => "Hello, Alice!" *)
let _ = greet ~prefix:"Hi" "Alice" (* => "Hi, Alice!" *)
(* 可选参数的类型是 'a option *)
let greet_v2 ?prefix name =
let p = match prefix with
| Some p -> p
| None -> "Hello"
in
p ^ ", " ^ name ^ "!"
(* 实际业务示例:HTTP 请求构造 *)
let make_request
?(method_ = "GET")
?(timeout = 30)
?(headers = [])
~url
() =
Printf.sprintf "%s %s (timeout=%d, headers=%d)"
method_ url timeout (List.length headers)
(* 调用 — 注意最后的 () 是 unit 参数 *)
let req = make_request ~url:"https://example.com" ()
(* => "GET https://example.com (timeout=30, headers=0)" *)
let req' = make_request ~method_:"POST" ~timeout:60
~url:"https://api.example.com/data" ()
(* => "POST https://api.example.com/data (timeout=60, headers=0)" *)
⚠️ 注意:可选参数后面通常需要一个 unit 参数 ()。这是因为 OCaml 需要知道何时所有可选参数都已提供,从而可以开始应用函数。没有 () 的话,如果最后一个可选参数未提供,编译器无法区分是部分应用还是完整调用。
可选参数的类型推导
let f ?x () = x
(* 类型: ?x:'a -> unit -> 'a option *)
let g ?(x=0) () = x
(* 类型: ?x:int -> unit -> int *)
函数应用操作符
|> 管道操作符
(* 管道操作符:将左边的值作为右边函数的参数 *)
let result =
[1; 2; 3; 4; 5]
|> List.map (fun x -> x * 2) (* [2; 4; 6; 8; 10] *)
|> List.filter (fun x -> x > 4) (* [6; 8; 10] *)
|> List.fold_left (+) 0 (* 24 *)
|> Printf.printf "Result: %d\n"
(* 等价于 *)
let result' =
Printf.printf "Result: %d\n"
(List.fold_left (+) 0
(List.filter (fun x -> x > 4)
(List.map (fun x -> x * 2)
[1; 2; 3; 4; 5])))
@@ 反向管道操作符
(* @@ 将右边的表达式作为左边函数的参数 *)
let () =
print_endline @@ Printf.sprintf "Hello, %s!" "World"
(* f @@ x @@ y 等价于 f (x y) *)
(* 等价于 *)
let () =
print_endline (Printf.sprintf "Hello, %s!" "World")
|> vs @@
| 操作符 | 方向 | 优先级 | 用法 |
|---|---|---|---|
|> | 数据从左流向右 | 低 | x |> f |> g |
@@ | 从右向左应用 | 低 | f @@ g @@ x |
💡 提示:|> 管道操作符在 OCaml 社区中非常流行,它使数据处理流水线更清晰易读。
高阶函数
接受或返回函数的函数称为高阶函数:
(* 接受函数作为参数 *)
let apply_twice f x = f (f x)
let _ = apply_twice (fun x -> x + 1) 5
(* => 7 *)
let _ = apply_twice (fun s -> s ^ "!") "hello"
(* => "hello!!" *)
(* 返回函数 *)
let make_adder n = fun x -> x + n
let add_10 = make_adder 10
let _ = add_10 5 (* => 15 *)
(* 组合函数 *)
let compose f g x = f (g x)
let add_one x = x + 1
let double x = x * 2
let double_then_add_one = compose add_one double
let _ = double_then_add_one 3 (* => 7 *)
常用高阶函数
(* List.map — 转换每个元素 *)
let squares = List.map (fun x -> x * x) [1; 2; 3; 4]
(* => [1; 4; 9; 16] *)
(* List.filter — 过滤元素 *)
let evens = List.filter (fun x -> x mod 2 = 0) [1; 2; 3; 4; 5; 6]
(* => [2; 4; 6] *)
(* List.fold_left — 累积计算 *)
let sum = List.fold_left (+) 0 [1; 2; 3; 4; 5]
(* => 15 *)
(* List.exists — 存在性检查 *)
let has_negative = List.exists (fun x -> x < 0) [1; -2; 3]
(* => true *)
(* List.for_all — 全称检查 *)
let all_positive = List.for_all (fun x -> x > 0) [1; 2; 3]
(* => true *)
实用示例:简单的函数组合库
(* toolkit.ml — 实用函数组合示例 *)
(* 管道风格的数据处理 *)
let process_users users =
users
|> List.filter (fun (_, age) -> age >= 18)
|> List.map (fun (name, age) -> Printf.sprintf "%s (%d岁)" name age)
|> String.concat ", "
(* 创建可配置的过滤器 *)
let make_range_filter ~min ~max =
fun x -> x >= min && x ~max
let filter_by_range = make_range_filter ~min:10 ~max:50
let _ = List.filter filter_by_range [5; 15; 25; 55]
(* => [15; 25] *)
(* 函数组合链 *)
let (>>) f g x = g (f x) (* 正向组合 *)
let process =
String.trim
>> String.lowercase_ascii
>> String.split_on_char ' '
>> List.filter (fun s -> s <> "")
let _ = process " Hello World OCaml "
(* => ["hello"; "world"; "ocaml"] *)
(* 带日志的函数包装 *)
let with_logging name f x =
Printf.printf "[%s] 输入: %d\n" name x;
let result = f x in
Printf.printf "[%s] 输出: %d\n" name result;
result
let () =
let logged_double = with_logging "double" (fun x -> x * 2) in
let logged_add = with_logging "add_one" (fun x -> x + 1) in
let pipeline = logged_double >> logged_add in
let _ = pipeline 5 in
(* 输出:
[double] 输入: 5
[double] 输出: 10
[add_one] 输入: 10
[add_one] 输出: 11
*)
()
业务场景
| 场景 | 推荐做法 |
|---|---|
| 数据转换管道 | 使用 |> 串联 List.map、filter 等 |
| 配置解析 | 使用可选参数 + 默认值 |
| 回调函数 | 使用命名参数 ~callback |
| 策略模式 | 传入不同函数作为参数 |
| 中间件 | 函数组合(compose) |