22. 说说你对柯里化的理解?
问题解析
柯里化(Currying)是函数式编程中的重要概念,它将一个多参数函数转化为一系列嵌套的单参数函数。理解柯里化的原理、实现方式以及应用场景,对于掌握函数式编程思想非常重要。
核心概念
什么是柯里化
柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程。它允许你将函数的参数分开传递,而不是一次性传递所有参数。
普通函数:
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
柯里化函数:
function add(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
add(1)(2)(3); // 6
柯里化的目的
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。
详细解答
柯里化的基本实现
二元函数的柯里化
// 普通函数
function getArea(width, height) {
return width * height;
}
// 柯里化版本
function getArea(width) {
return function(height) {
return width * height;
};
}
// 使用
const getTenWidthArea = getArea(10);
getTenWidthArea(20); // 200
getTenWidthArea(30); // 300
// 偶尔宽度变化也可以轻松复用
const getTwentyWidthArea = getArea(20);
getTwentyWidthArea(20); // 400
通用柯里化函数
// 将任意函数柯里化
function curry(fn) {
return function curried(...args) {
// 如果参数足够,直接执行
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 参数不够,返回一个新函数,继续收集参数
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// 使用
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
curriedSum(1, 2, 3); // 6
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 6
ES6箭头函数版本
const curry = fn =>
curried = (...args) =>
args.length >= fn.length
? fn(...args)
: (...nextArgs) => curried(...args, ...nextArgs);
// 更简洁的二元函数柯里化
const add = a => b => a + b;
const multiply = a => b => a * b;
const add5 = add(5);
add5(3); // 8
const double = multiply(2);
double(5); // 10
柯里化的应用
1. 参数复用
// 创建一个通用的请求函数
const request = curry((baseUrl, endpoint, method, data) => {
return fetch(`${baseUrl}${endpoint}`, {
method,
body: JSON.stringify(data)
});
});
// 预设baseUrl
const apiRequest = request('https://api.example.com');
// 预设endpoint
const userRequest = apiRequest('/users');
// 预设method
const getUser = userRequest('GET');
const createUser = userRequest('POST');
// 使用
getUser(null); // GET请求
createUser({ name: 'John', email: 'john@example.com' }); // POST请求
2. 函数组合
const curry = fn =>
curried = (...args) =>
args.length >= fn.length
? fn(...args)
: (...nextArgs) => curried(...args, ...nextArgs);
const map = curry((fn, arr) => arr.map(fn));
const filter = curry((fn, arr) => arr.filter(fn));
const reduce = curry((fn, initial, arr) => arr.reduce(fn, initial));
// 使用
const numbers = [1, 2, 3, 4, 5, 6];
const isEven = x => x % 2 === 0;
const double = x => x * 2;
const sum = (a, b) => a + b;
// 组合操作
const result = reduce(sum, 0)(map(double)(filter(isEven)(numbers)));
// 等价于:先过滤偶数,再翻倍,最后求和
// [2, 4, 6] -> [4, 8, 12] -> 24
3. 延迟执行
const log = curry((level, message) => {
console.log(`[${level}] ${message}`);
});
const info = log('INFO');
const warn = log('WARN');
const error = log('ERROR');
// 延迟到需要时才执行
info('Application started');
warn('Low memory');
error('Connection failed');
深入理解
偏函数应用 vs 柯里化
// 柯里化:将多参数函数转为嵌套的单参数函数
const add = a => b => c => a + b + c;
// 偏函数应用:固定部分参数,返回接受剩余参数的函数
const partialAdd = (a, b) => c => a + b + c;
// 使用bind实现偏函数
function add(a, b, c) {
return a + b + c;
}
const add5 = add.bind(null, 5); // 固定第一个参数为5
add5(2, 3); // 10
无限柯里化
// 实现无限参数的柯里化
function infiniteCurry(fn) {
const next = (...args) => {
return (x) => {
if (x === undefined) {
return fn(...args);
}
return next(...args, x);
};
};
return next();
}
const sum = infiniteCurry((...numbers) =>
numbers.reduce((a, b) => a + b, 0)
);
sum(1)(2)(3)(4)(); // 10
sum(1)(2)(3)(4)(5)(); // 15
实际应用:验证函数
const curry = fn =>
curried = (...args) =>
args.length >= fn.length
? fn(...args)
: (...nextArgs) => curried(...args, ...nextArgs);
// 验证规则
const required = (fieldName, value) => {
return value ? null : `${fieldName} is required`;
};
const minLength = curry((length, fieldName, value) => {
return value.length >= length
? null
: `${fieldName} must be at least ${length} characters`;
});
const email = (fieldName, value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value)
? null
: `${fieldName} must be a valid email`;
};
// 预设验证器
const minLength3 = minLength(3);
const minLength8 = minLength(8);
// 验证函数
const validate = (validators, data) => {
const errors = {};
for (const [field, value] of Object.entries(data)) {
for (const validator of validators[field] || []) {
const error = validator(field, value);
if (error) {
errors[field] = error;
break;
}
}
}
return errors;
};
// 使用
const validators = {
username: [required, minLength3],
password: [required, minLength8],
email: [required, email]
};
const data = {
username: 'jo',
password: '12345',
email: 'invalid-email'
};
validate(validators, data);
// { username: 'username must be at least 3 characters',
// password: 'password must be at least 8 characters',
// email: 'email must be a valid email' }
最佳实践
1. 使用Lodash的curry
import { curry } from 'lodash';
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
2. 与函数组合结合
import { curry, flow, map, filter } from 'lodash/fp';
const isEven = x => x % 2 === 0;
const double = x => x * 2;
const processNumbers = flow(
filter(isEven),
map(double)
);
processNumbers([1, 2, 3, 4, 5, 6]); // [4, 8, 12]
3. 避免过度柯里化
// 过度柯里化:难以阅读
const process = a => b => c => d => e => a + b + c + d + e;
// 更好的做法:根据实际使用场景决定
const process = (a, b) => c => (d, e) => a + b + c + d + e;
// 前两个参数经常一起使用,中间参数单独使用,后两个参数一起使用
面试要点
- 定义:柯里化是将多参数函数转化为嵌套单参数函数的过程
- 目的:参数复用、延迟执行、函数组合
- 实现:能够手写柯里化函数
- 应用场景:API预设、验证规则、数据处理管道
- 与偏函数区别:柯里化是单参数化,偏函数是固定部分参数
常见问题
Q:什么是柯里化? A:柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程,允许分批次传递参数。
Q:柯里化有什么作用? A:主要作用包括参数复用、延迟执行、便于函数组合。可以创建配置化的函数,提高代码复用性。
Q:如何实现一个通用的柯里化函数? A:通过递归检查参数个数,如果参数足够则执行函数,否则返回新函数继续收集参数。
Q:柯里化和偏函数有什么区别? A:柯里化是将函数转为嵌套的单参数函数,偏函数是固定部分参数返回接受剩余参数的函数。