强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 -> intint * int -> int
调用f a bf (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.mapfilter
配置解析使用可选参数 + 默认值
回调函数使用命名参数 ~callback
策略模式传入不同函数作为参数
中间件函数组合(compose

扩展阅读