函子与单子
函子(Functor)和单子(Monad)是函数式编程中最具代表性的抽象概念。它们源于范畴论,但在编程实践中,我们可以从实用的角度来理解和使用它们,而不必深入数学理论。
为什么需要这些抽象?
在开始之前,让我们先理解为什么需要函子和单子这样的抽象。
在编程中,我们经常处理"包装在容器中的值":
- 数组:多个值的容器
- Promise:未来值的容器
- Maybe/Either:可能不存在或可能出错的值的容器
这些容器有一些共同的操作模式:我们想对容器里的值进行转换,但不想手动拆开容器。函子和单子就是为这种场景设计的统一接口。
函子(Functor)
什么是函子?
函子是一个实现了 map 方法的容器类型。map 方法让你可以在不离开容器的情况下,对容器内的值进行转换。
用更数学的语言来说,函子是一种类型构造器,它实现了将一个函数应用到包装值上的能力。
函子的定义
一个类型要成为函子,必须满足:
- 有一个类型构造器,用于包装值
- 有一个
map方法,接受一个函数,返回新的函子
// 函子接口(概念性的,JavaScript 没有接口)
interface Functor<T> {
map<U>(fn: (value: T) => U): Functor<U>;
}
最简单的函子:Identity
// Identity 函子:最简单的容器
class Identity {
constructor(value) {
this.value = value;
}
// 静态工厂方法
static of(value) {
return new Identity(value);
}
// map 方法:对容器内的值应用函数
map(fn) {
return Identity.of(fn(this.value));
}
// 提取值
get() {
return this.value;
}
}
// 使用
const result = Identity.of(5)
.map(x => x + 1)
.map(x => x * 2)
.get();
console.log(result); // 12
函子定律
函子必须遵守两条定律,确保行为可预测:
定律一:同一律(Identity)
如果传入的函数是恒等函数(x => x),则返回的函子应该与原函子相同:
// 同一律:map(id) = id
const id = x => x;
const functor = Identity.of(5);
const mapped = functor.map(id);
console.log(mapped.get() === functor.get()); // true
定律二:组合律(Composition)
对函子连续应用两个函数,应该等于先组合这两个函数再应用:
// 组合律:map(f).map(g) = map(x => g(f(x)))
const f = x => x + 1;
const g = x => x * 2;
const functor = Identity.of(5);
// 方式一:连续 map
const way1 = functor.map(f).map(g);
// 方式二:组合后 map
const way2 = functor.map(x => g(f(x)));
console.log(way1.get() === way2.get()); // true(都是 12)
这些定律保证函子的行为是可预测、可组合的。
JavaScript 中的函子
数组是函子
数组是最常见的函子实现:
const numbers = [1, 2, 3, 4, 5];
// 数组的 map 方法符合函子定义
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// 验证同一律
const id = x => x;
console.log([1, 2, 3].map(id)); // [1, 2, 3](与原数组相同)
// 验证组合律
const f = x => x + 1;
const g = x => x * 2;
const arr = [1, 2, 3];
const way1 = arr.map(f).map(g); // [4, 6, 8]
const way2 = arr.map(x => g(f(x))); // [4, 6, 8]
Promise 也是函子
Promise 的 then 方法类似于 map:
Promise.resolve(5)
.then(x => x + 1) // 类似于 map
.then(x => x * 2)
.then(console.log); // 12
函子的价值
函子的真正价值在于它提供了一种统一的操作容器的方式。无论容器是什么类型(数组、Promise、Maybe),都可以用 map 来处理内部的值。
// 对不同容器用相同方式操作
const processArray = arr => arr.map(x => x * 2);
const processPromise = p => p.then(x => x * 2);
const processIdentity = i => i.map(x => x * 2);
// 如果有统一接口,可以用同一个函数处理
const process = functor => functor.map(x => x * 2);
单子(Monad)
什么是单子?
单子是函子的扩展。它解决了一个函子无法优雅处理的问题:嵌套的容器。
考虑这个场景:
// 假设有一个可能失败的异步操作
const fetchUser = id =>
id > 0 ? Promise.resolve({ id, name: '张三' }) : Promise.reject('无效ID');
// 假设获取用户后还要获取他的帖子
const fetchPosts = user =>
user.id ? Promise.resolve([{ title: '帖子1' }, { title: '帖子2' }]) : Promise.reject('无帖子');
// 如果直接用 map,会得到 Promise<Promise<Post[]>>
const nested = fetchUser(1).map(user => fetchPosts(user));
// 这会得到嵌套的 Promise,很麻烦
// 使用 then(相当于 flatMap),会自动展平
const flat = fetchUser(1).then(user => fetchPosts(user));
// 直接得到 Promise<Post[]>
单子提供了 flatMap(也叫 chain、bind 或 >>=)方法,可以将嵌套的容器展平。
单子的定义
一个类型要成为单子,必须满足:
- 是一个函子(有
map方法) - 有一个
of(也叫unit或return)方法,用于将值包装进容器 - 有一个
flatMap(也叫chain、bind)方法,用于处理返回容器的操作
// 单子接口(概念性的)
interface Monad<T> extends Functor<T> {
static of<T>(value: T): Monad<T>;
flatMap<U>(fn: (value: T) => Monad<U>): Monad<U>;
}
简单单子实现
class Box {
constructor(value) {
this.value = value;
}
// 包装值
static of(value) {
return new Box(value);
}
// 函子的 map
map(fn) {
return Box.of(fn(this.value));
}
// 单子的 flatMap
flatMap(fn) {
return fn(this.value);
}
// 提取值
fold(onValue) {
return onValue(this.value);
}
}
// 使用 map vs flatMap
const box = Box.of(5);
// map 返回包装后的值
const mapped = box.map(x => x + 1);
console.log(mapped); // Box { value: 6 }
// flatMap 用于函数返回 Box 的情况
const flatMapped = box.flatMap(x => Box.of(x + 1));
console.log(flatMapped); // Box { value: 6 }
// 为什么需要 flatMap?
// 当操作可能失败时
const safeDivide = (a, b) =>
b === 0 ? Box.of(null) : Box.of(a / b);
// 使用 flatMap 链式调用
const result = Box.of(10)
.flatMap(x => safeDivide(x, 2)) // Box(5)
.flatMap(x => safeDivide(x, 0)); // Box(null)
console.log(result); // Box { value: null }
单子定律
单子必须遵守三条定律:
定律一:左同一律(Left Identity)
// of(a).flatMap(f) 等于 f(a)
const f = x => Box.of(x * 2);
const a = 5;
const left = Box.of(a).flatMap(f);
const right = f(a);
console.log(left.value === right.value); // true
定律二:右同一律(Right Identity)
// m.flatMap(of) 等于 m
const m = Box.of(5);
const result = m.flatMap(Box.of);
console.log(result.value === m.value); // true
定律三:结合律(Associativity)
// m.flatMap(f).flatMap(g) 等于 m.flatMap(x => f(x).flatMap(g))
const f = x => Box.of(x + 1);
const g = x => Box.of(x * 2);
const m = Box.of(5);
const left = m.flatMap(f).flatMap(g);
const right = m.flatMap(x => f(x).flatMap(g));
console.log(left.value === right.value); // true(都是 12)
实用单子示例
Maybe 单子
Maybe 单子用于安全处理可能为 null 或 undefined 的值:
class Maybe {
static of(value) {
return value === null || value === undefined
? new Nothing()
: new Just(value);
}
static just(value) {
return new Just(value);
}
static nothing() {
return new Nothing();
}
}
class Just extends Maybe {
constructor(value) {
super();
this.value = value;
}
map(fn) {
return Maybe.of(fn(this.value));
}
flatMap(fn) {
return fn(this.value);
}
getOrElse(defaultValue) {
return this.value;
}
isJust() { return true; }
isNothing() { return false; }
}
class Nothing extends Maybe {
map(fn) {
return this; // 无值时,直接返回自身
}
flatMap(fn) {
return this; // 无值时,直接返回自身
}
getOrElse(defaultValue) {
return defaultValue;
}
isJust() { return false; }
isNothing() { return true; }
}
// 实际使用
const user = {
profile: {
address: {
city: '北京'
}
}
};
const getUserCity = user =>
Maybe.of(user)
.flatMap(u => Maybe.of(u.profile))
.flatMap(p => Maybe.of(p.address))
.map(a => a.city)
.getOrElse('未知');
console.log(getUserCity(user)); // '北京'
console.log(getUserCity({})); // '未知'
console.log(getUserCity(null)); // '未知'
// 不使用 Maybe 的写法(冗长且容易出错)
const getUserCityOld = user => {
if (!user) return '未知';
if (!user.profile) return '未知';
if (!user.profile.address) return '未知';
if (!user.profile.address.city) return '未知';
return user.profile.address.city;
};
Either 单子
Either 单子用于处理可能出错的操作,并能携带错误信息:
class Either {
static left(value) {
return new Left(value);
}
static right(value) {
return new Right(value);
}
static fromNullable(value) {
return value === null || value === undefined
? Either.left(value)
: Either.right(value);
}
static tryCatch(fn) {
try {
return Either.right(fn());
} catch (e) {
return Either.left(e);
}
}
}
class Left extends Either {
constructor(value) {
super();
this.value = value;
}
map(fn) {
return this; // Left 不执行转换
}
flatMap(fn) {
return this; // Left 不执行转换
}
fold(onLeft, onRight) {
return onLeft(this.value);
}
getOrElse(defaultValue) {
return defaultValue;
}
isLeft() { return true; }
isRight() { return false; }
}
class Right extends Either {
constructor(value) {
super();
this.value = value;
}
map(fn) {
return Either.right(fn(this.value));
}
flatMap(fn) {
return fn(this.value);
}
fold(onLeft, onRight) {
return onRight(this.value);
}
getOrElse(defaultValue) {
return this.value;
}
isLeft() { return false; }
isRight() { return true; }
}
// 实际使用
const parseJSON = str =>
Either.tryCatch(() => JSON.parse(str));
const result1 = parseJSON('{"name": "张三"}');
console.log(result1.fold(
error => `解析失败: ${error.message}`,
data => `解析成功: ${data.name}`
)); // 解析成功: 张三
const result2 = parseJSON('invalid json');
console.log(result2.fold(
error => `解析失败: ${error.message}`,
data => `解析成功: ${data.name}`
)); // 解析失败: ...
// 链式验证
const validateAge = age =>
age < 0 ? Either.left('年龄不能为负数') :
age > 150 ? Either.left('年龄不合理') :
Either.right(age);
const validateName = name =>
!name || name.trim() === '' ? Either.left('姓名不能为空') :
Either.right(name.trim());
const validateUser = data =>
validateName(data.name)
.flatMap(name =>
validateAge(data.age)
.map(age => ({ name, age }))
);
console.log(validateUser({ name: '张三', age: 25 }));
// Right { value: { name: '张三', age: 25 } }
console.log(validateUser({ name: '', age: 25 }));
// Left { value: '姓名不能为空' }
console.log(validateUser({ name: '张三', age: -5 }));
// Left { value: '年龄不能为负数' }
Task 单子(异步操作)
Task 单子用于处理异步操作,类似于 Promise 但更可控:
class Task {
constructor(fn) {
this.fn = fn;
}
static of(value) {
return new Task((reject, resolve) => resolve(value));
}
static rejected(error) {
return new Task((reject, resolve) => reject(error));
}
map(fn) {
return new Task((reject, resolve) =>
this.fn(
reject,
value => resolve(fn(value))
)
);
}
flatMap(fn) {
return new Task((reject, resolve) =>
this.fn(
reject,
value => fn(value).fn(reject, resolve)
)
);
}
run(onRejected, onResolved) {
return this.fn(onRejected, onResolved);
}
}
// 使用
const fetchUser = id =>
new Task((reject, resolve) => {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(resolve)
.catch(reject);
});
const fetchPosts = userId =>
new Task((reject, resolve) => {
fetch(`/api/posts?userId=${userId}`)
.then(res => res.json())
.then(resolve)
.catch(reject);
});
// 组合异步操作
fetchUser(1)
.flatMap(user => fetchPosts(user.id))
.map(posts => posts.map(p => p.title))
.run(
error => console.error('错误:', error),
result => console.log('结果:', result)
);
常见单子模式
链式操作
单子最强大的能力是链式操作,每一步都可能失败或返回容器:
// 读取配置、连接数据库、查询数据的链式操作
const loadConfig = Either.tryCatch(() =>
JSON.parse(fs.readFileSync('config.json'))
);
const connectDB = config =>
Either.tryCatch(() => dbDriver.connect(config.connectionString));
const queryData = connection =>
Either.tryCatch(() => connection.query('SELECT * FROM users'));
const result = loadConfig()
.flatMap(connectDB)
.flatMap(queryData)
.fold(
error => console.error('操作失败:', error),
data => console.log('查询结果:', data)
);
防御式编程
使用单子进行防御式编程,避免空指针错误:
// 安全获取嵌套属性
const safeGet = (...keys) => obj =>
keys.reduce(
(maybe, key) => maybe.flatMap(o => Maybe.of(o[key])),
Maybe.of(obj)
);
const data = {
user: {
profile: {
settings: {
theme: 'dark'
}
}
}
};
const getTheme = safeGet('user', 'profile', 'settings', 'theme');
console.log(getTheme(data).getOrElse('light')); // 'dark'
console.log(getTheme({}).getOrElse('light')); // 'light'
小结
函子和单子是函数式编程中处理"容器中的值"的抽象:
函子:
- 实现了
map方法的容器 - 允许对容器内的值进行转换,不离开容器
- 遵守同一律和组合律
单子:
- 扩展了函子,增加了
flatMap方法 - 解决嵌套容器的问题
- 提供链式操作的能力
实用单子:
- Maybe:安全处理可能为空的值
- Either:携带错误信息的失败处理
- Task:可控的异步操作
理解函子和单子的价值不在于实现它们,而在于理解这种抽象模式。它们提供了一种统一的方式来处理各种"上下文"中的值,让代码更加健壮、可组合、易于推理。