返回首页

35. 说说你对函数柯里化和组合函数的理解?

问题解析

函数柯里化(Currying)和函数组合(Function Composition)是函数式编程中的两个核心概念。它们可以提高代码的复用性、可读性和可维护性。面试中主要考察它们的定义、实现方式、使用场景以及相互关系。

核心概念

1. 函数柯里化(Currying)

柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的过程。

// 普通函数
function add(a, b, c) {
  return a + b + c;
}
console.log(add(1, 2, 3)); // 6

// 柯里化版本
function addCurried(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
console.log(addCurried(1)(2)(3)); // 6

// 箭头函数简化
const addCurriedArrow = a => b => c => a + b + c;
console.log(addCurriedArrow(1)(2)(3)); // 6

2. 函数组合(Function Composition)

函数组合是将多个函数组合成一个新函数,新函数的输出是前一个函数的输入。

// 两个函数组合
const compose = (f, g) => x => f(g(x));

// 使用示例
const double = x => x * 2;
const increment = x => x + 1;

const doubleThenIncrement = compose(increment, double);
console.log(doubleThenIncrement(5)); // 11 (5 * 2 + 1)

const incrementThenDouble = compose(double, increment);
console.log(incrementThenDouble(5)); // 12 ((5 + 1) * 2)

详细解答

1. 柯里化的实现

// 手动柯里化
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, d) {
  return a + b + c + d;
}

const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)(4));      // 10
console.log(curriedSum(1, 2)(3)(4));      // 10
console.log(curriedSum(1)(2, 3, 4));      // 10
console.log(curriedSum(1, 2, 3, 4));      // 10

// 偏函数应用(Partial Application)
const add5 = curriedSum(5);
console.log(add5(1)(2)(3)); // 11

// 带占位符的柯里化
function curryWithPlaceholder(fn, placeholder = '_') {
  return function curried(...args) {
    const hasPlaceholder = args.some(arg => arg === placeholder);

    if (args.length >= fn.length && !hasPlaceholder) {
      return fn.apply(this, args);
    }

    return function(...nextArgs) {
      const mergedArgs = args.map(arg =>
        arg === placeholder && nextArgs.length ? nextArgs.shift() : arg
      );
      return curried.apply(this, [...mergedArgs, ...nextArgs]);
    };
  };
}

const greet = curryWithPlaceholder((greeting, name, punctuation) => {
  return `${greeting}, ${name}${punctuation}`;
});

const sayHelloTo = greet('Hello', '_', '!');
console.log(sayHelloTo('Alice')); // "Hello, Alice!"

2. 函数组合的实现

// 从右到左组合(数学中的 compose)
const compose = (...fns) => {
  return fns.reduce((f, g) => (...args) => f(g(...args)));
};

// 从左到右组合(pipe)
const pipe = (...fns) => {
  return fns.reduce((f, g) => (...args) => g(f(...args)));
};

// 使用示例
const toUpper = str => str.toUpperCase();
const exclaim = str => str + '!';
const repeat = str => str + ' ' + str;

// compose:从右到左执行
const composeResult = compose(toUpper, exclaim, repeat);
console.log(composeResult('hello')); // "HELLO HELLO!"
// 执行顺序:repeat -> exclaim -> toUpper

// pipe:从左到右执行
const pipeResult = pipe(toUpper, exclaim, repeat);
console.log(pipeResult('hello')); // "HELLO! HELLO!"
// 执行顺序:toUpper -> exclaim -> repeat

// 带调试的组合
const trace = label => value => {
  console.log(`${label}:`, value);
  return value;
};

const debugPipe = pipe(
  trace('input'),
  x => x + 1,
  trace('after add'),
  x => x * 2,
  trace('after double')
);

debugPipe(5);
// input: 5
// after add: 6
// after double: 12

3. 柯里化与组合的结合

// 组合柯里化函数
const map = curry((fn, arr) => arr.map(fn));
const filter = curry((predicate, arr) => arr.filter(predicate));
const reduce = curry((fn, initial, arr) => arr.reduce(fn, initial));

// 数据处理管道
const users = [
  { name: 'Alice', age: 25, active: true },
  { name: 'Bob', age: 30, active: false },
  { name: 'Charlie', age: 35, active: true }
];

// 使用 compose 构建数据处理流程
const getActiveUserNames = compose(
  map(user => user.name),
  filter(user => user.active),
  filter(user => user.age > 25)
);

console.log(getActiveUserNames(users)); // ['Charlie']

// 更复杂的组合
const sum = reduce((a, b) => a + b, 0);
const getTotalAgeOfActiveUsers = compose(
  sum,
  map(user => user.age),
  filter(user => user.active)
);

console.log(getTotalAgeOfActiveUsers(users)); // 60 (25 + 35)

深入理解

1. 柯里化的原理和优势

// 柯里化的核心:延迟执行和参数复用

// 1. 参数复用
const multiply = curry((a, b) => a * b);
const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// 2. 延迟执行
const ajax = curry((method, url, data) => {
  return fetch(url, {
    method,
    body: JSON.stringify(data)
  });
});

const post = ajax('POST');
const postToAPI = post('https://api.example.com');

// 稍后调用
postToAPI({ name: 'Alice' });

// 3. 动态创建函数
const checkAge = curry((min, max, age) => age >= min && age <= max);
const isAdult = checkAge(18, 65);
const isSenior = checkAge(65, 120);

console.log(isAdult(25));  // true
console.log(isSenior(70)); // true

2. 函数组合的数学基础

// 函数组合满足结合律
const f = x => x + 1;
const g = x => x * 2;
const h = x => x - 3;

// compose(f, compose(g, h)) === compose(compose(f, g), h)
const left = compose(f, compose(g, h));
const right = compose(compose(f, g), h);

console.log(left(5));  // ((5 - 3) * 2) + 1 = 5
console.log(right(5)); // ((5 - 3) * 2) + 1 = 5

// 单位元(Identity)
const identity = x => x;
const withIdentity = compose(f, identity);
console.log(withIdentity(5)); // f(5) = 6

// 实现更强大的 compose
const composeAdvanced = (...fns) => {
  if (fns.length === 0) return identity;
  if (fns.length === 1) return fns[0];

  return fns.reduce((f, g) => (...args) => {
    const result = g(...args);
    // 支持 Promise
    if (result && typeof result.then === 'function') {
      return result.then(f);
    }
    return f(result);
  });
};

// 支持异步函数
const asyncPipe = pipe(
  x => Promise.resolve(x + 1),
  async x => x * 2,
  x => x + 3
);

asyncPipe(5).then(console.log); // (5 + 1) * 2 + 3 = 15

3. Point-Free 风格

// Point-Free:不使用参数的函数定义风格

// 非 Point-Free
const isEven = n => n % 2 === 0;
const getEvens = arr => arr.filter(isEven);

// Point-Free
const filter = curry((fn, arr) => arr.filter(fn));
const getEvensPF = filter(isEven);

console.log(getEvensPF([1, 2, 3, 4, 5, 6])); // [2, 4, 6]

// 更多 Point-Free 示例
const prop = curry((key, obj) => obj[key]);
const eq = curry((a, b) => a === b);
const not = fn => (...args) => !fn(...args);

// 获取所有年龄不为 25 的用户名称
const getNames = map(prop('name'));
const ageIs25 = compose(eq(25), prop('age'));
const ageIsNot25 = not(ageIs25);

const getNamesOfNot25 = compose(
  getNames,
  filter(ageIsNot25)
);

console.log(getNamesOfNot25(users)); // ['Bob', 'Charlie']

最佳实践

1. 实用工具函数库

// 常用柯里化工具函数
const R = {
  // 柯里化
  curry: fn => {
    const curried = (...args) =>
      args.length >= fn.length
        ? fn(...args)
        : (...next) => curried(...args, ...next);
    return curried;
  },

  // 组合
  compose: (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))),
  pipe: (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))),

  // 常用函数
  map: curry((fn, arr) => arr.map(fn)),
  filter: curry((fn, arr) => arr.filter(fn)),
  reduce: curry((fn, init, arr) => arr.reduce(fn, init)),
  find: curry((fn, arr) => arr.find(fn)),
  prop: curry((key, obj) => obj[key]),
  pick: curry((keys, obj) => keys.reduce((acc, key) => {
    if (key in obj) acc[key] = obj[key];
    return acc;
  }, {})),
  pluck: curry((key, arr) => arr.map(obj => obj[key])),

  // 逻辑函数
  and: curry((a, b) => a && b),
  or: curry((a, b) => a || b),
  not: fn => (...args) => !fn(...args),

  // 比较函数
  eq: curry((a, b) => a === b),
  gt: curry((a, b) => b > a),
  lt: curry((a, b) => b < a),

  // 条件函数
  ifElse: curry((condition, onTrue, onFalse, value) =>
    condition(value) ? onTrue(value) : onFalse(value)
  ),

  // 恒等函数
  identity: x => x,
  always: x => () => x,
  tap: curry((fn, x) => { fn(x); return x; })
};

// 使用示例
const { compose, map, filter, prop, gt, pluck } = R;

const getAdultNames = compose(
  pluck('name'),
  filter(compose(gt(18), prop('age')))
);

console.log(getAdultNames(users)); // ['Alice', 'Bob', 'Charlie']

2. 实际应用场景

// 1. 数据验证管道
const validate = pipe(
  trim,
  toLowerCase,
  removeSpecialChars,
  checkLength(3, 20),
  checkUnique
);

// 2. 数据处理管道
const processUserData = pipe(
  filterActive,
  sortByAge,
  map(formatUser),
  take(10)
);

// 3. React/Redux 中的使用
const mapStateToProps = compose(
  pick(['user', 'settings']),
  mergeWithDefaults,
  normalizeData
);

// 4. 表单验证
const validateEmail = ifElse(
  matchesEmailPattern,
  always({ valid: true }),
  always({ valid: false, error: 'Invalid email' })
);

// 5. 日志和监控
const withLogging = fn => compose(
  tap(result => console.log('Result:', result)),
  fn,
  tap(args => console.log('Args:', args))
);

3. 与 Lodash/Ramda 的对比

// Lodash/fp(函数式编程版本)
import { flow, map, filter, curry } from 'lodash/fp';

const process = flow(
  filter(user => user.active),
  map(user => user.name),
  names => names.join(', ')
);

// Ramda
import { compose, map, filter, prop, gt } from 'ramda';

const processR = compose(
  join(', '),
  map(prop('name')),
  filter(user => gt(prop('age', user), 18))
);

// 自定义轻量版
const myProcess = pipe(
  filter(prop('active')),
  map(prop('name')),
  join(', ')
);

4. 性能考虑

// 1. 避免过度组合
// 不好:每次调用都创建新函数
const process = data => {
  return compose(
    map(fn1),
    filter(fn2),
    reduce(fn3, init)
  )(data);
};

// 好:预定义组合函数
const processOptimized = compose(
  map(fn1),
  filter(fn2),
  reduce(fn3, init)
);

// 2. 记忆化
const memoize = fn => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key);
  };
};

const expensiveOperation = memoize(compose(
  complexCalculation,
  dataTransformation
));

// 3. 短路求值
const lazyFilter = curry((predicate, arr) => function* () {
  for (const item of arr) {
    if (predicate(item)) yield item;
  }
});

面试要点

  1. 柯里化定义:将多参数函数转换为单参数函数链
  2. 函数组合定义:将多个函数组合成一个新函数
  3. compose vs pipe:compose 从右到左,pipe 从左到右
  4. Point-Free 风格:不使用参数的函数定义方式
  5. 实际应用:数据处理管道、验证、React/Redux

常见面试题

// 面试题 1:实现柯里化函数
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...args2) {
      return curried.apply(this, args.concat(args2));
    };
  };
}

// 面试题 2:实现 compose 和 pipe
const compose = (...fns) =>
  fns.reduce((f, g) => (...args) => f(g(...args)));

const pipe = (...fns) =>
  fns.reduce((f, g) => (...args) => g(f(...args)));

// 面试题 3:柯里化和偏函数应用的区别?
// 柯里化:将 f(a,b,c) 转换为 f(a)(b)(c)
// 偏函数应用:固定部分参数,返回接受剩余参数的函数

// 偏函数实现
const partial = (fn, ...fixedArgs) =>
  (...remainingArgs) => fn(...fixedArgs, ...remainingArgs);

const add = (a, b, c) => a + b + c;
const add5 = partial(add, 5);
console.log(add5(2, 3)); // 10

// 面试题 4:使用柯里化和组合实现数据处理
const users = [
  { name: 'Alice', age: 25, score: 80 },
  { name: 'Bob', age: 30, score: 90 },
  { name: 'Charlie', age: 35, score: 85 }
];

const curry = fn =>
  function curried(...args) {
    return args.length >= fn.length
      ? fn(...args)
      : (...next) => curried(...args, ...next);
  };

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const filter = curry((fn, arr) => arr.filter(fn));
const map = curry((fn, arr) => arr.map(fn));
const reduce = curry((fn, init, arr) => arr.reduce(fn, init));
const prop = curry((key, obj) => obj[key]);
const gt = curry((a, b) => b > a);

// 获取所有年龄大于 25 的用户的平均分
const averageScore = pipe(
  filter(compose(gt(25), prop('age'))),
  map(prop('score')),
  scores => scores.reduce((a, b) => a + b, 0) / scores.length
);

console.log(averageScore(users)); // 87.5

// 面试题 5:实现一个支持 Promise 的 compose
const composeAsync = (...fns) =>
  fns.reduce((f, g) => async (...args) => f(await g(...args)));

const asyncPipe = (...fns) =>
  fns.reduce((f, g) => async (...args) => g(await f(...args)));

// 使用
const fetchUser = id => Promise.resolve({ id, name: 'Alice' });
const uppercaseName = user => ({ ...user, name: user.name.toUpperCase() });
const logUser = user => { console.log(user); return user; };

const processUser = asyncPipe(
  fetchUser,
  uppercaseName,
  logUser
);

processUser(1); // { id: 1, name: 'ALICE' }