函数式编程艺术 / 04 一等公民函数
04 一等公民函数
“函数是一等公民——它可以被传递、被返回、被赋值给变量,就像数字和字符串一样。”
4.1 函数是一等公民
当一门语言将函数视为一等公民(First-Class Citizen),意味着函数与其他值(数字、字符串、对象)享有同等地位。
4.1.1 一等公民的能力
| 能力 | 说明 | 示例 |
|---|---|---|
| 赋值给变量 | 函数可绑定到变量 | const f = (x) => x + 1 |
| 作为参数传递 | 函数可传给其他函数 | [1,2].map(x => x * 2) |
| 作为返回值 | 函数可从函数返回 | const add = (x) => (y) => x + y |
| 存储在数据结构中 | 函数可放入数组/对象 | {add: (a,b) => a+b} |
| 匿名创建 | 无需命名即可创建 | lambda x: x + 1 |
4.1.2 各语言的函数表示
Haskell:
-- Haskell 中函数天然是一等公民
f :: Int -> Int
f = \x -> x + 1
-- 函数列表
funcs :: [Int -> Int]
funcs = [(+1), (*2), (^2)]
-- 应用函数列表
result :: [Int]
result = map ($ 5) funcs -- [6, 10, 25]
JavaScript:
// 赋值给变量
const double = (x) => x * 2;
// 作为参数
const apply = (fn, x) => fn(x);
apply(double, 5); // 10
// 作为返回值
const multiply = (a) => (b) => a * b;
const triple = multiply(3);
triple(10); // 30
// 存储在数据结构中
const operations = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
};
operations.add(2, 3); // 5
Python:
# 赋值给变量
double = lambda x: x * 2
# 作为参数
def apply(fn, x):
return fn(x)
apply(double, 5) # 10
# 作为返回值
def multiply(a):
return lambda b: a * b
triple = multiply(3)
triple(10) # 30
# 存储在数据结构中
operations = {
'add': lambda a, b: a + b,
'sub': lambda a, b: a - b,
'mul': lambda a, b: a * b,
}
operations['add'](2, 3) # 5
Rust:
// 赋值给变量
let double = |x: i32| x * 2;
// 作为参数
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
apply(double, 5); // 10
// 作为返回值(闭包)
fn multiply(a: i32) -> impl Fn(i32) -> i32 {
move |b| a * b
}
let triple = multiply(3);
triple(10); // 30
// 存储在 HashMap 中
use std::collections::HashMap;
let mut ops: HashMap<&str, Box<dyn Fn(i32, i32) -> i32>> = HashMap::new();
ops.insert("add", Box::new(|a, b| a + b));
Clojure:
;; 赋值给变量
(def double (fn [x] (* x 2)))
;; 作为参数
(defn apply-fn [f x] (f x))
(apply-fn double 5) ;; 10
;; 作为返回值
(defn multiply [a]
(fn [b] (* a b)))
(def triple (multiply 3))
(triple 10) ;; 30
;; 存储在 map 中
(def operations
{:add (fn [a b] (+ a b))
:sub (fn [a b] (- a b))
:mul (fn [a b] (* a b))})
((:add operations) 2 3) ;; 5
4.2 高阶函数(Higher-Order Function)
高阶函数是接收函数作为参数或返回函数作为结果的函数。
4.2.1 常见高阶函数
| 高阶函数 | 类型签名 (Haskell) | 用途 |
|---|---|---|
map | (a -> b) -> [a] -> [b] | 转换每个元素 |
filter | (a -> Bool) -> [a] -> [a] | 筛选元素 |
reduce/fold | (b -> a -> b) -> b -> [a] -> b | 归约为单个值 |
sort | (a -> a -> Ordering) -> [a] -> [a] | 自定义排序 |
compose | (b -> c) -> (a -> b) -> a -> c | 函数组合 |
curry | ((a, b) -> c) -> a -> b -> c | 柯里化 |
partial | - | 偏应用 |
memoize | - | 缓存函数结果 |
4.2.2 自定义高阶函数
JavaScript:
// retry:重试函数
const retry = (fn, maxAttempts = 3) => async (...args) => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(...args);
} catch (err) {
if (attempt === maxAttempts) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 100));
}
}
};
// once:只执行一次
const once = (fn) => {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
};
// tap:调试用,不改变值
const tap = (fn) => (x) => {
fn(x);
return x;
};
// 使用示例
const result = [1, 2, 3, 4, 5]
.map(x => x * 2)
.filter(x => x > 4)
.reduce((sum, x) => sum + x, 0);
// result = 24
Haskell:
-- retry
retry :: Int -> IO a -> IO a
retry 0 action = action
retry n action = action `catch` \(_ :: SomeException) -> retry (n-1) action
-- once(使用 IORef)
once :: IO a -> IO (IO a)
once action = do
ref <- newIORef Nothing
return $ do
cached <- readIORef ref
case cached of
Just val -> return val
Nothing -> do
val <- action
writeIORef ref (Just val)
return val
-- tap
tap :: (a -> IO ()) -> a -> IO a
tap f x = f x >> return x
4.3 函数组合(Function Composition)
函数组合是将多个函数串联起来形成新函数的技术。
4.3.1 数学定义
(f ∘ g)(x) = f(g(x))
即:先执行 g,再执行 f
4.3.2 各语言的组合方式
Haskell:
-- 使用 . 运算符
-- f . g = \x -> f (g x)
-- 从右到左组合
addOne :: Int -> Int
addOne = (+1)
double :: Int -> Int
double = (*2)
-- doubleAfterAddOne = double . addOne
-- doubleAfterAddOne x = double (addOne x)
doubleAfterAddOne :: Int -> Int
doubleAfterAddOne = double . addOne
-- doubleAfterAddOne 3 == 8 (3+1=4, 4*2=8)
-- 多函数组合
pipeline :: String -> String
pipeline = unwords . map (take 10) . words . map toLower
JavaScript:
// 手动组合
const compose = (...fns) =>
(x) => fns.reduceRight((acc, fn) => fn(acc), x);
// 管道(从左到右)
const pipe = (...fns) =>
(x) => fns.reduce((acc, fn) => fn(acc), x);
// 使用
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const toString = (x) => x.toString();
const doubleAfterAddOne = compose(double, addOne);
doubleAfterAddOne(3); // 8
const pipeline = pipe(addOne, double, toString);
pipeline(3); // "8"
// Ramda 库
const R = require('ramda');
const transform = R.pipe(
R.map(R.prop('name')),
R.filter(name => name.length > 3),
R.sortBy(R.identity),
R.join(', ')
);
Python:
from functools import reduce
# 手动组合
def compose(*fns):
return lambda x: reduce(lambda acc, fn: fn(acc), reversed(fns), x)
# 管道
def pipe(*fns):
return lambda x: reduce(lambda acc, fn: fn(acc), fns, x)
add_one = lambda x: x + 1
double = lambda x: x * 2
to_str = lambda x: str(x)
double_after_add_one = compose(double, add_one)
double_after_add_one(3) # 8
pipeline = pipe(add_one, double, to_str)
pipeline(3) # "8"
# toolz 库
from toolz import pipe, compose
from toolz.curried import map, filter, pipe
Rust:
// Rust 没有内置组合,但可以用函数式方法链
let result: i32 = (1..=5)
.map(|x| x + 1)
.filter(|x| x > &3)
.sum();
// 或使用自定义组合宏
macro_rules! compose {
($f:expr) => { $f };
($f:expr, $($rest:expr),+) => {
move |x| compose!($($rest),+)( $f(x) )
};
}
fn add_one(x: i32) -> i32 { x + 1 }
fn double(x: i32) -> i32 { x * 2 }
let f = compose!(add_one, double);
f(3) // (3*2)+1 = 7
Clojure:
;; 使用 comp(从右到左)
(def double-after-add-one (comp #(* 2 %) #(+ 1 %)))
(double-after-add-one 3) ;; 8
;; 使用 -> 和 ->> 宏(从左到右)
(-> 3
(+ 1) ;; 4
(* 2) ;; 8
str) ;; "8"
;; 管道操作
(->> [1 2 3 4 5]
(map #(* 2 %)) ;; (2 4 6 8 10)
(filter #(> % 4)) ;; (6 8 10)
(reduce +)) ;; 24
4.3.3 组合定律
函数组合满足以下定律:
| 定律 | 表达式 | 含义 |
|---|---|---|
| 结合律 | (f ∘ g) ∘ h ≡ f ∘ (g ∘ h) | 组合顺序不影响结果 |
| 单位元 | id ∘ f ≡ f ∘ id ≡ f | 恒等函数是组合的单位元 |
-- 结合律验证
-- (f . g) . h = f . (g . h)
-- 单位元
id :: a -> a
id x = x
-- id . f = f
-- f . id = f
4.4 柯里化(Currying)
柯里化是将多参数函数转换为一系列单参数函数的技术。
4.4.1 定义
未柯里化: f(a, b, c) → result
柯里化后: f(a)(b)(c) → result
等价类型转换: (a, b) → c ≡ a → (b → c)
4.4.2 各语言实现
Haskell:
-- Haskell 中函数天然柯里化
add :: Int -> Int -> Int
add x y = x + y
-- 等价于
-- add :: Int -> (Int -> Int)
-- add = \x -> \y -> x + y
add3 :: Int -> Int
add3 = add 3 -- 部分应用
add3 5 -- 8
add3 10 -- 13
-- map 也是柯里化的
map :: (a -> b) -> [a] -> [b]
-- 部分应用 map
doubleAll :: [Int] -> [Int]
doubleAll = map (*2)
JavaScript:
// 手动柯里化
const curry = (fn) => {
const arity = fn.length;
const curried = (...args) =>
args.length >= arity
? fn(...args)
: (...moreArgs) => curried(...args, ...moreArgs);
return curried;
};
// 使用
const add = curry((a, b) => a + b);
const add3 = add(3);
add3(5); // 8
add3(10); // 13
// 多参数柯里化
const sum3 = curry((a, b, c) => a + b + c);
sum3(1)(2)(3); // 6
sum3(1, 2)(3); // 6
sum3(1)(2, 3); // 6
sum3(1, 2, 3); // 6
// Ramda 自动柯里化
const R = require('ramda');
const multiply = R.multiply(3);
multiply(10); // 30
Python:
from functools import partial
# 使用 partial(偏应用)
def add(a, b):
return a + b
add3 = partial(add, 3)
add3(5) # 8
add3(10) # 13
# 手动柯里化装饰器
def curry(fn):
import inspect
arity = len(inspect.signature(fn).parameters)
def curried(*args):
if len(args) >= arity:
return fn(*args)
return lambda *more: curried(*args, *more)
return curried
@curry
def sum3(a, b, c):
return a + b + c
sum3(1)(2)(3) # 6
sum3(1, 2)(3) # 6
Rust:
// Rust 不原生支持柯里化,但可以手动实现
fn add(a: i32) -> impl Fn(i32) -> i32 {
move |b| a + b
}
let add3 = add(3);
add3(5); // 8
add3(10); // 13
// 宏实现柯里化
macro_rules! curry {
($name:ident, $first:ident : $fty:ty, $($rest:ident : $rty:ty),* -> $ret:ty, $body:expr) => {
fn $name($first: $fty) -> impl Fn($($rty),*) -> $ret {
move |$($rest),*| $body
}
};
}
Clojure:
;; Clojure 不自动柯里化,但可以手动实现
(defn curry2 [f]
(fn [a]
(fn [b]
(f a b))))
(def add (curry2 +))
((add 3) 5) ;; 8
;; 更实用的方式:使用 partial
(def add3 (partial + 3))
(add3 5) ;; 8
(add3 10) ;; 13
;; partial 可以部分应用任意数量参数
(def add-1-2 (partial + 1 2))
(add-1-2 3) ;; 6
4.5 偏应用(Partial Application)
偏应用是固定函数的部分参数,产生一个接受剩余参数的新函数。
4.5.1 柯里化 vs 偏应用
| 特性 | 柯里化 | 偏应用 |
|---|---|---|
| 参数传递 | 一次一个 | 一次可传多个 |
| 返回函数 | 每次返回单参数函数 | 返回接受剩余参数的函数 |
| 灵活性 | 更灵活 | 更直接 |
4.5.2 实用偏应用
JavaScript:
// 日志函数的偏应用
const log = (level, timestamp, component, message) =>
`[${timestamp}] [${level}] [${component}] ${message}`;
// 固定级别和组件
const infoLog = (component) => (message) =>
log('INFO', new Date().toISOString(), component, message);
const authLog = infoLog('AuthService');
authLog('User logged in'); // [2026-01-01T...] [INFO] [AuthService] User logged in
// 事件处理偏应用
const handleEvent = (eventType, handler) => (event) => {
if (event.type === eventType) handler(event);
};
const handleClick = handleEvent('click', (e) => {
console.log('Clicked at', e.clientX, e.clientY);
});
document.addEventListener('click', handleClick);
Python:
from functools import partial
# HTTP 客户端的偏应用
def http_request(method, base_url, path, headers=None):
url = f"{base_url}{path}"
headers = headers or {}
return {'method': method, 'url': url, 'headers': headers}
# 创建专用请求函数
api_request = partial(http_request, 'GET', 'https://api.example.com')
get_users = partial(api_request, '/users')
get_user = lambda uid: partial(api_request, f'/users/{uid}')()
get_users() # {'method': 'GET', 'url': 'https://api.example.com/users', ...}
get_user(42) # {'method': 'GET', 'url': 'https://api.example.com/users/42', ...}
4.6 函数组合的进阶模式
4.6.1 管道(Pipeline)
// 数据处理管道
const pipeline = (...steps) => (input) =>
steps.reduce((data, step) => step(data), input);
// 用户注册处理管道
const processRegistration = pipeline(
validateInput,
normalizeEmail,
checkDuplicate,
hashPassword,
createUserRecord,
sendWelcomeEmail
);
const result = await processRegistration({
email: ' Alice@Example.COM ',
password: 'secret123',
name: 'Alice'
});
4.6.2 组合子(Combinator)
// 组合子:通过组合产生新行为的函数
// identity
const I = (x) => x;
// constant
const K = (x) => (_y) => x;
// substitution
const S = (f) => (g) => (x) => f(x)(g(x));
// flip
const flip = (f) => (a) => (b) => f(b)(a);
// on
const on = (f) => (g) => (a) => (b) => f(g(a))(g(b));
// 使用:按长度比较字符串
const compareByLength = on((a) => (b) => a - b)(s => s.length);
compareByLength('abc')('de'); // 1 (3 - 2 = 1)
4.6.3 点自由风格(Point-Free Style)
-- 有点(显式参数)
isEven :: Int -> Bool
isEven x = x `mod` 2 == 0
-- 无点(Point-free)
isEven :: Int -> Bool
isEven = (== 0) . (`mod` 2)
-- 有点
sumOfSquares :: [Int] -> Int
sumOfSquares xs = sum (map (^2) xs)
-- 无点
sumOfSquares :: [Int] -> Int
sumOfSquares = sum . map (^2)
// JavaScript 中的 point-free
// 有点
const getActiveUserNames = (users) =>
users.filter(u => u.active).map(u => u.name);
// 无点(使用 Ramda)
const getActiveUserNames = R.pipe(
R.filter(R.prop('active')),
R.map(R.prop('name'))
);
4.7 业务场景
4.7.1 中间件系统
// Express/Koa 风格的中间件(函数组合的典型应用)
const compose = (...middlewares) =>
(context) => {
let index = -1;
const dispatch = (i) => {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(context, () => dispatch(i + 1)));
};
return dispatch(0);
};
// 纯函数中间件
const logging = async (ctx, next) => {
console.log(`[${ctx.method}] ${ctx.path}`);
await next();
};
const auth = async (ctx, next) => {
if (!ctx.headers.authorization) throw new Error('Unauthorized');
ctx.user = decodeToken(ctx.headers.authorization);
await next();
};
const cors = async (ctx, next) => {
ctx.headers['Access-Control-Allow-Origin'] = '*';
await next();
};
const app = compose(cors, logging, auth);
4.7.2 数据转换管道
# ETL 管道使用函数组合
from functools import reduce
from datetime import datetime
def compose(*fns):
return lambda x: reduce(lambda acc, fn: fn(acc), fns, x)
# 各步骤都是纯函数
def parse_csv(raw_data):
return [line.split(',') for line in raw_data.strip().split('\n')]
def skip_header(rows):
return rows[1:]
def parse_dates(rows, date_col=2):
return [
[*row[:date_col], datetime.strptime(row[date_col], '%Y-%m-%d'), *row[date_col+1:]]
for row in rows
]
def filter_valid(rows):
return [row for row in rows if row[3] > 0] # amount > 0
def summarize(rows):
from collections import defaultdict
summary = defaultdict(float)
for row in rows:
summary[row[1]] += float(row[3])
return dict(summary)
# 组合管道
process_data = compose(
parse_csv,
skip_header,
lambda rows: parse_dates(rows),
filter_valid,
summarize
)
result = process_data(raw_csv_data)
4.8 注意事项
| 注意事项 | 说明 |
|---|---|
| Point-free 过度 | 适度使用,过于隐晦降低可读性 |
| 柯里化的性能 | 多次函数调用可能有开销 |
| 参数顺序 | 柯里化时参数顺序很重要,数据参数应最后 |
| this 绑定 | JavaScript 中注意箭头函数和 this |
| 类型推断 | 某些语言中高阶函数影响类型推断 |
4.9 小结
| 要点 | 说明 |
|---|---|
| 一等公民 | 函数可赋值、传递、返回、存储 |
| 高阶函数 | 接收或返回函数的函数 |
| 函数组合 | f ∘ g,将函数串联成管道 |
| 柯里化 | 多参数函数 → 多个单参数函数 |
| 偏应用 | 固定部分参数,返回新函数 |
扩展阅读
- 《Mostly Adequate Guide to FP》 — 第 5 章 Composition
- Ramda.js 文档 — 函数式工具库
- Functional-Light JS — 第 3 章 Managing Function Inputs
- Haskell 函数组合 — Haskell Wiki
下一章:05 Map/Filter/Reduce — 函数式数据处理的核心操作