Skip to content

函数组合进阶:构建优雅的数据处理管道

组合的艺术

在音乐中, 单个音符很简单, 但当它们按特定顺序组合时, 就能创造出美妙的旋律。函数组合也是如此——将简单的函数组合起来, 构建出强大而优雅的程序。

函数组合(Function Composition)是函数式编程的核心思想之一。它让我们能够:

  • 将复杂问题分解为简单函数
  • 通过组合构建新功能
  • 创建清晰的数据转换管道
  • 编写可复用、可测试的代码

基础组合模式

1. Compose 和 Pipe

在基础函数式编程中, 我们已经了解了 composepipe。让我们深入探讨它们:

javascript
// Compose: 从右到左执行 (数学函数组合的方向)
const compose =
  (...fns) =>
  (value) =>
    fns.reduceRight((acc, fn) => fn(acc), value);

// Pipe: 从左到右执行 (更直观, Unix 管道风格)
const pipe =
  (...fns) =>
  (value) =>
    fns.reduce((acc, fn) => fn(acc), value);

// 基础函数
const double = (x) => x * 2;
const increment = (x) => x + 1;
const square = (x) => x * x;

// 使用 compose: 从右到左
const calc1 = compose(
  square, // 3. 最后平方
  increment, // 2. 然后加1
  double // 1. 首先翻倍
);

console.log(calc1(3)); // (3 * 2 + 1)² = 49

// 使用 pipe: 从左到右 (更易读)
const calc2 = pipe(
  double, // 1. 首先翻倍
  increment, // 2. 然后加1
  square // 3. 最后平方
);

console.log(calc2(3)); // (3 * 2 + 1)² = 49

2. 多参数函数的组合

javascript
// 问题: 只有第一个函数可以接收多个参数
const add = (a, b) => a + b;
const multiply = (x) => x * 2;
const subtract = (x) => x - 1;

// ❌ 这样不行
const bad = pipe(add, multiply, subtract);
// bad(5, 3) => NaN (因为 multiply 和 subtract 只能接收一个参数)

// ✅ 柯里化解决方案
const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...nextArgs) => curried(...args, ...nextArgs);
  };
};

const curriedAdd = curry((a, b) => a + b);

const good = pipe(
  curriedAdd, // 可以分步接收参数
  multiply,
  subtract
);

console.log(good(5)(3)); // ((5 + 3) * 2) - 1 = 15

// 或者一开始就设计为单参数函数
const addN = (n) => (x) => x + n;
const multiplyN = (n) => (x) => x * n;
const subtractN = (n) => (x) => x - n;

const calc = pipe(addN(3), multiplyN(2), subtractN(1));

console.log(calc(5)); // ((5 + 3) * 2) - 1 = 15

Point-Free 风格

Point-Free 风格是指函数定义中不显式提及数据参数:

javascript
// ❌ Pointed 风格 - 显式提及参数
const getNames = (users) => users.map((user) => user.name);

// ✅ Point-Free 风格 - 不提及参数
const prop = (key) => (obj) => obj[key];
const map = (fn) => (array) => array.map(fn);

const getNames = map(prop("name"));

// 更复杂的例子
const users = [
  { name: "John", age: 25, active: true },
  { name: "Jane", age: 30, active: false },
  { name: "Bob", age: 35, active: true },
];

// Pointed 风格
const getActiveUsersNames = (users) =>
  users
    .filter((user) => user.active)
    .map((user) => user.name)
    .map((name) => name.toUpperCase());

// Point-Free 风格
const filter = (predicate) => (array) => array.filter(predicate);
const toUpperCase = (str) => str.toUpperCase();

const getActiveUsersNamesPF = pipe(
  filter(prop("active")),
  map(prop("name")),
  map(toUpperCase)
);

console.log(getActiveUsersNames(users)); // ['JOHN', 'BOB']
console.log(getActiveUsersNamesPF(users)); // ['JOHN', 'BOB']

Point-Free 风格的优势:

  1. 更简洁, 减少样板代码
  2. 更易组合和复用
  3. 关注数据转换而非数据本身
  4. 更易测试(不需要模拟数据)

高级组合模式

1. Transducer (转换器)

Transducer 是一种高效的组合多个转换操作的方法, 避免创建中间数组:

javascript
// 传统方式 - 创建多个中间数组
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const traditional = numbers
  .map((x) => x * 2) // 中间数组 1
  .filter((x) => x > 10) // 中间数组 2
  .map((x) => x + 1); // 最终数组

console.log(traditional); // [11, 13, 15, 17, 19, 21]

// Transducer 方式 - 单次遍历
const mapT = (fn) => (reducer) => (acc, value) => reducer(acc, fn(value));

const filterT = (predicate) => (reducer) => (acc, value) =>
  predicate(value) ? reducer(acc, value) : acc;

const transduce = (transducer, reducer, initial, collection) => {
  const xf = transducer(reducer);
  return collection.reduce(xf, initial);
};

const transducer = pipe(
  mapT((x) => x * 2),
  filterT((x) => x > 10),
  mapT((x) => x + 1)
);

const result = transduce(
  transducer,
  (acc, value) => [...acc, value],
  [],
  numbers
);

console.log(result); // [11, 13, 15, 17, 19, 21]

2. 异步组合

处理异步操作的组合:

javascript
// 异步 pipe
const asyncPipe =
  (...fns) =>
  (value) =>
    fns.reduce(async (acc, fn) => fn(await acc), Promise.resolve(value));

// 异步操作
const fetchUser = async (id) => {
  console.log(`Fetching user ${id}...`);
  await new Promise((resolve) => setTimeout(resolve, 100));
  return { id, name: "John", email: "[email protected]" };
};

const enrichWithPosts = async (user) => {
  console.log(`Fetching posts for ${user.name}...`);
  await new Promise((resolve) => setTimeout(resolve, 100));
  return {
    ...user,
    posts: [
      { id: 1, title: "Hello World" },
      { id: 2, title: "JavaScript Tips" },
    ],
  };
};

const enrichWithComments = async (user) => {
  console.log(`Fetching comments for ${user.name}...`);
  await new Promise((resolve) => setTimeout(resolve, 100));
  return {
    ...user,
    comments: [
      { id: 1, text: "Great post!" },
      { id: 2, text: "Thanks for sharing" },
    ],
  };
};

const addTimestamp = (user) => ({
  ...user,
  fetchedAt: new Date(),
});

// 组合异步操作
const getUserWithAllData = asyncPipe(
  fetchUser,
  enrichWithPosts,
  enrichWithComments,
  addTimestamp
);

getUserWithAllData(1).then((user) => {
  console.log("完整用户数据:", user);
});

3. 条件组合

根据条件选择不同的函数进行组合:

javascript
// 条件执行
const when = (predicate, fn) => (value) => predicate(value) ? fn(value) : value;

const unless = (predicate, fn) => when((value) => !predicate(value), fn);

// 使用示例
const isNegative = (x) => x < 0;
const isLarge = (x) => x > 100;
const isDecimal = (x) => !Number.isInteger(x);

const processNumber = pipe(
  when(isNegative, Math.abs), // 转为正数
  when(isLarge, () => 100), // 限制最大值
  unless(Number.isInteger, Math.floor) // 取整
);

console.log(processNumber(-50)); // 50
console.log(processNumber(150)); // 100
console.log(processNumber(45.7)); // 45
console.log(processNumber(30)); // 30

分支组合:

javascript
// 根据条件选择不同的处理流程
const branch = (predicate, trueFn, falseFn) => (value) =>
  predicate(value) ? trueFn(value) : falseFn(value);

const cond =
  (...pairs) =>
  (value) => {
    for (const [predicate, fn] of pairs) {
      if (predicate(value)) {
        return fn(value);
      }
    }
    return value;
  };

// 使用示例
const categorizeAge = cond(
  [(age) => age < 13, () => "child"],
  [(age) => age < 20, () => "teenager"],
  [(age) => age < 60, () => "adult"],
  [() => true, () => "senior"]
);

console.log(categorizeAge(10)); // 'child'
console.log(categorizeAge(16)); // 'teenager'
console.log(categorizeAge(35)); // 'adult'
console.log(categorizeAge(70)); // 'senior'

4. 收集器组合

处理聚合操作:

javascript
// 创建收集器
const collector = {
  toArray: () => (acc, value) => [...acc, value],
  toSet: () => (acc, value) => acc.add(value),
  toObject: (keyFn, valueFn) => (acc, value) => ({
    ...acc,
    [keyFn(value)]: valueFn(value),
  }),
  groupBy: (keyFn) => (acc, value) => {
    const key = keyFn(value);
    return {
      ...acc,
      [key]: [...(acc[key] || []), value],
    };
  },
};

// 使用
const users = [
  { id: 1, name: "John", role: "admin" },
  { id: 2, name: "Jane", role: "user" },
  { id: 3, name: "Bob", role: "admin" },
];

// 转换为对象
const usersMap = users.reduce(
  collector.toObject(
    (u) => u.id,
    (u) => u
  ),
  {}
);

console.log(usersMap);
// { '1': {id: 1, ...}, '2': {id: 2, ...}, '3': {id: 3, ...} }

// 按角色分组
const groupedByRole = users.reduce(
  collector.groupBy((u) => u.role),
  {}
);

console.log(groupedByRole);
// { admin: [{id: 1, ...}, {id: 3, ...}], user: [{id: 2, ...}] }

实战场景

1. 数据验证管道

javascript
// 构建灵活的验证系统
const validate = {
  required: (field) => (value) => ({
    valid: value != null && value !== "",
    error: value ? null : `${field} 是必填项`,
  }),

  minLength: (field, min) => (value) => ({
    valid: value && value.length >= min,
    error: value?.length >= min ? null : `${field} 至少需要 ${min} 个字符`,
  }),

  email: (field) => (value) => ({
    valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    error: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
      ? null
      : `${field} 格式不正确`,
  }),

  range: (field, min, max) => (value) => ({
    valid: value >= min && value <= max,
    error:
      value >= min && value <= max
        ? null
        : `${field} 必须在 ${min}-${max} 之间`,
  }),
};

// 组合验证器
const composeValidators =
  (...validators) =>
  (value) => {
    const results = validators.map((v) => v(value));
    const firstError = results.find((r) => !r.valid);

    return {
      valid: !firstError,
      error: firstError?.error || null,
      errors: results.filter((r) => !r.valid).map((r) => r.error),
    };
  };

// 使用
const validateUsername = composeValidators(
  validate.required("用户名"),
  validate.minLength("用户名", 3)
);

const validateEmail = composeValidators(
  validate.required("邮箱"),
  validate.email("邮箱")
);

const validateAge = composeValidators(
  validate.required("年龄"),
  validate.range("年龄", 18, 100)
);

console.log(validateUsername("")); // { valid: false, error: '...' }
console.log(validateUsername("ab")); // { valid: false, error: '...' }
console.log(validateUsername("john")); // { valid: true, error: null }

console.log(validateEmail("invalid")); // { valid: false, error: '...' }
console.log(validateEmail("[email protected]")); // { valid: true, error: null }

2. 数据转换管道

javascript
// 构建复杂的数据处理流程
const transform = {
  filterBy: (key, value) => (items) =>
    items.filter((item) => item[key] === value),

  sortBy:
    (key, order = "asc") =>
    (items) =>
      [...items].sort((a, b) =>
        order === "asc" ? (a[key] > b[key] ? 1 : -1) : a[key] < b[key] ? 1 : -1
      ),

  mapTo: (fn) => (items) => items.map(fn),

  take: (n) => (items) => items.slice(0, n),

  unique: (key) => (items) => {
    const seen = new Set();
    return items.filter((item) => {
      const value = item[key];
      if (seen.has(value)) return false;
      seen.add(value);
      return true;
    });
  },
};

// 电商订单处理
const orders = [
  {
    id: 1,
    customer: "John",
    amount: 150,
    status: "pending",
    date: "2025-01-01",
  },
  {
    id: 2,
    customer: "Jane",
    amount: 200,
    status: "completed",
    date: "2025-01-02",
  },
  {
    id: 3,
    customer: "Bob",
    amount: 100,
    status: "pending",
    date: "2025-01-03",
  },
  {
    id: 4,
    customer: "Alice",
    amount: 300,
    status: "completed",
    date: "2025-01-04",
  },
  {
    id: 5,
    customer: "John",
    amount: 250,
    status: "pending",
    date: "2025-01-05",
  },
];

// 获取待处理订单, 按金额降序, 取前3个, 格式化输出
const processOrders = pipe(
  transform.filterBy("status", "pending"),
  transform.sortBy("amount", "desc"),
  transform.take(3),
  transform.mapTo((order) => ({
    id: order.id,
    summary: `订单 #${order.id} - ${order.customer}`,
    amount: `$${order.amount}`,
  }))
);

console.log(processOrders(orders));

3. React Hooks 组合

javascript
// 组合 React Hooks 创建自定义 Hook
const composeHooks = (...hooks) => (...args) => {
  return hooks.reduce((acc, hook) => {
    const result = hook(...args);
    return { ...acc, ...result };
  }, {});
};

// 示例 Hooks
const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  const updateValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };

  return { value, setValue: updateValue };
};

const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return { debouncedValue };
};

// 组合使用
const useDebounced LocalStorage = composeHooks(
  useLocalStorage,
  (key, initialValue) => useDebounce(initialValue, 500)
);

4. 中间件管道

javascript
// Express 风格的中间件组合
const composeMiddleware = (...middlewares) => {
  return async (context) => {
    let index = 0;

    const next = async () => {
      if (index >= middlewares.length) return;

      const middleware = middlewares[index++];
      await middleware(context, next);
    };

    await next();
    return context;
  };
};

// 中间件示例
const logger = async (ctx, next) => {
  console.log(`-> ${ctx.method} ${ctx.path}`);
  await next();
  console.log(`<- ${ctx.method} ${ctx.path} ${ctx.status}`);
};

const auth = async (ctx, next) => {
  if (!ctx.user) {
    ctx.status = 401;
    ctx.body = { error: "Unauthorized" };
    return;
  }
  await next();
};

const rateLimit = async (ctx, next) => {
  // 简化的限流逻辑
  if (Math.random() < 0.1) {
    ctx.status = 429;
    ctx.body = { error: "Too many requests" };
    return;
  }
  await next();
};

const handler = async (ctx, next) => {
  ctx.status = 200;
  ctx.body = { message: "Success", data: { id: 1 } };
  await next();
};

// 组合中间件
const app = composeMiddleware(logger, auth, rateLimit, handler);

// 使用
app({
  method: "GET",
  path: "/api/users",
  user: { id: 1, name: "John" },
}).then((result) => {
  console.log("响应:", result);
});

性能优化

1. 惰性求值

javascript
// 延迟执行直到需要结果
class LazySequence {
  constructor(data) {
    this.data = data;
    this.operations = [];
  }

  map(fn) {
    this.operations.push({ type: "map", fn });
    return this;
  }

  filter(fn) {
    this.operations.push({ type: "filter", fn });
    return this;
  }

  take(n) {
    this.operations.push({ type: "take", n });
    return this;
  }

  // 只有调用 toArray 时才执行
  toArray() {
    let result = this.data;

    for (const op of this.operations) {
      if (op.type === "map") {
        result = result.map(op.fn);
      } else if (op.type === "filter") {
        result = result.filter(op.fn);
      } else if (op.type === "take") {
        result = result.slice(0, op.n);
      }
    }

    return result;
  }
}

const lazy = (data) => new LazySequence(data);

// 使用
const numbers = Array.from({ length: 1000000 }, (_, i) => i);

const result = lazy(numbers)
  .map((x) => x * 2)
  .filter((x) => x > 100)
  .take(5)
  .toArray(); // 只有这时才开始执行

console.log(result);

2. 函数记忆化组合

javascript
// 为组合函数添加记忆化
const memoizedCompose = (...fns) => {
  const cache = new Map();
  const composed = compose(...fns);

  return (value) => {
    const key = JSON.stringify(value);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = composed(value);
    cache.set(key, result);

    return result;
  };
};

小结

函数组合是函数式编程的精髓, 它让我们能够:

  1. 分解复杂问题: 将大问题拆分为小函数
  2. 构建数据管道: pipe/compose 创建清晰的处理流程
  3. 提高可复用性: 小函数可以在不同场景组合
  4. 增强可测试性: 每个小函数独立测试
  5. 优雅的抽象: Point-Free 风格和高级模式

关键技术:

  • 基础: compose、pipe
  • 高级: transducer、异步组合、条件组合
  • 优化: 惰性求值、记忆化
  • 实践: 验证、转换、中间件