返回首页

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;
// 前两个参数经常一起使用,中间参数单独使用,后两个参数一起使用

面试要点

  1. 定义:柯里化是将多参数函数转化为嵌套单参数函数的过程
  2. 目的:参数复用、延迟执行、函数组合
  3. 实现:能够手写柯里化函数
  4. 应用场景:API预设、验证规则、数据处理管道
  5. 与偏函数区别:柯里化是单参数化,偏函数是固定部分参数

常见问题

Q:什么是柯里化? A:柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程,允许分批次传递参数。

Q:柯里化有什么作用? A:主要作用包括参数复用、延迟执行、便于函数组合。可以创建配置化的函数,提高代码复用性。

Q:如何实现一个通用的柯里化函数? A:通过递归检查参数个数,如果参数足够则执行函数,否则返回新函数继续收集参数。

Q:柯里化和偏函数有什么区别? A:柯里化是将函数转为嵌套的单参数函数,偏函数是固定部分参数返回接受剩余参数的函数。