您现在的位置是:网站首页 > 柯里化模式(Currying)的函数式编程应用文章详情

柯里化模式(Currying)的函数式编程应用

柯里化模式的概念

柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。它得名于数学家Haskell Curry,是函数式编程中的重要概念。柯里化后的函数每次只接受一个参数,并返回一个新函数来接收下一个参数,直到所有参数都被收集完毕才执行最终计算。

// 普通函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化版本
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    }
  }
}

console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

JavaScript中的柯里化实现

在JavaScript中实现柯里化有多种方式,下面介绍几种常见方法:

手动柯里化

function multiply(a) {
  return function(b) {
    return function(c) {
      return a * b * c;
    }
  }
}

const double = multiply(2);
const triple = double(3);
console.log(triple(4)); // 24

通用柯里化函数

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));
      }
    }
  }
}

const curriedSum = curry((a, b, c) => a + b + c);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6

ES6箭头函数实现

const curry = fn => 
  judge = (...args) =>
    args.length >= fn.length
      ? fn(...args)
      : (...arg) => judge(...args, ...arg);

const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6

柯里化的实际应用场景

参数复用

// 创建URL
function createURL(base, path, query) {
  return `${base}/${path}?${query}`;
}

// 柯里化版本
function createURLCurried(base) {
  return function(path) {
    return function(query) {
      return `${base}/${path}?${query}`;
    }
  }
}

const createGithubURL = createURLCurried('https://github.com');
const createRepoURL = createGithubURL('username/repo');
console.log(createRepoURL('tab=repositories')); 
// https://github.com/username/repo?tab=repositories

延迟执行

// 事件处理
const handleClick = id => event => {
  console.log(`Clicked element with id: ${id}`);
  console.log('Event:', event);
};

document.getElementById('btn').addEventListener(
  'click', 
  handleClick('my-button')
);

函数组合

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const toUpperCase = str => str.toUpperCase();
const exclaim = str => `${str}!`;
const repeat = str => `${str} ${str}`;

const transform = compose(repeat, exclaim, toUpperCase);
console.log(transform('hello')); // HELLO! HELLO!

高级柯里化技巧

占位符功能

function curryWithPlaceholder(fn) {
  return function curried(...args) {
    // 过滤占位符
    const actualArgs = args.slice(0, fn.length);
    const hasPlaceholder = actualArgs.includes(curryWithPlaceholder.placeholder);
    
    if (actualArgs.length >= fn.length && !hasPlaceholder) {
      return fn.apply(this, actualArgs);
    } else {
      return function(...args2) {
        // 替换占位符
        const mergedArgs = args.map(arg => 
          arg === curryWithPlaceholder.placeholder && args2.length 
            ? args2.shift() 
            : arg
        ).concat(args2);
        return curried.apply(this, mergedArgs);
      }
    }
  }
}

curryWithPlaceholder.placeholder = Symbol();

const _ = curryWithPlaceholder.placeholder;
const fn = curryWithPlaceholder((a, b, c, d) => [a, b, c, d]);

console.log(fn(1)(2)(3)(4)); // [1, 2, 3, 4]
console.log(fn(1, _, 3)(2)(4)); // [1, 2, 3, 4]
console.log(fn(1, _, _, 4)(2)(3)); // [1, 2, 3, 4]

自动柯里化

function autoCurry(fn, arity = fn.length) {
  return function curried(...args) {
    if (args.length >= arity) {
      return fn(...args);
    }
    return autoCurry(fn.bind(null, ...args), arity - args.length);
  }
}

const add = autoCurry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2, 3)); // 6

柯里化与部分应用的比较

柯里化和部分应用(Partial Application)经常被混淆,但它们有本质区别:

// 部分应用
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  }
}

// 柯里化
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 greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`;
}

// 部分应用
const sayHello = partial(greet, 'Hello');
console.log(sayHello('World', '!')); // "Hello, World!"

// 柯里化
const curriedGreet = curry(greet);
console.log(curriedGreet('Hello')('World')('!')); // "Hello, World!"

性能考量与优化

柯里化会带来一定的性能开销,主要体现在:

  1. 每次柯里化调用都会创建新的闭包
  2. 参数需要多次传递
  3. 调用栈深度增加

优化策略:

// 1. 避免过度柯里化
// 不好的做法:所有函数都柯里化
// 好的做法:只在需要时柯里化

// 2. 使用惰性柯里化
function lazyCurry(fn) {
  let args = [];
  return function curried(...newArgs) {
    args = args.concat(newArgs);
    if (args.length >= fn.length) {
      const result = fn(...args);
      args = [];
      return result;
    }
    return curried;
  }
}

// 3. 使用记忆化
function memoizedCurry(fn) {
  const cache = new Map();
  return function curried(...args) {
    const key = args.join(',');
    if (cache.has(key)) {
      return cache.get(key);
    }
    if (args.length >= fn.length) {
      const result = fn(...args);
      cache.set(key, result);
      return result;
    } else {
      const next = (...args2) => curried(...args, ...args2);
      cache.set(key, next);
      return next;
    }
  }
}

柯里化在函数组合中的应用

函数组合是函数式编程的核心概念,柯里化使其更加灵活:

// 工具函数
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const map = f => arr => arr.map(f);
const filter = f => arr => arr.filter(f);
const reduce = (f, init) => arr => arr.reduce(f, init);

// 柯里化数据处理管道
const processData = pipe(
  filter(x => x.age > 18),
  map(x => ({ ...x, name: x.name.toUpperCase() })),
  reduce((acc, curr) => acc + curr.score, 0)
);

const data = [
  { name: 'Alice', age: 20, score: 85 },
  { name: 'Bob', age: 17, score: 65 },
  { name: 'Charlie', age: 22, score: 90 }
];

console.log(processData(data)); // 175 (85 + 90)

柯里化与React组件

在React中,柯里化可以用于创建高阶组件或处理事件:

// 柯里化事件处理器
const withLogging = (message) => (handler) => (event) => {
  console.log(message);
  handler(event);
};

const Button = ({ onClick, children }) => {
  const handleClick = withLogging('Button clicked')(onClick);
  
  return (
    <button onClick={handleClick}>
      {children}
    </button>
  );
};

// 使用
const App = () => {
  const handleClick = () => {
    console.log('Original click handler');
  };
  
  return (
    <Button onClick={handleClick}>
      Click Me
    </Button>
  );
};

柯里化与Redux

在Redux中,柯里化常用于action创建函数和中间件:

// 柯里化action创建函数
const createAction = type => payload => ({ type, payload });

const addTodo = createAction('ADD_TODO');
const removeTodo = createAction('REMOVE_TODO');

console.log(addTodo('Learn currying')); 
// { type: 'ADD_TODO', payload: 'Learn currying' }

// 柯里化中间件
const loggerMiddleware = store => next => action => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('Next state:', store.getState());
  return result;
};

// 柯里化reducer
const createReducer = (initialState, handlers) => 
  (state = initialState, action) => 
    handlers[action.type] 
      ? handlers[action.type](state, action) 
      : state;

const todoReducer = createReducer([], {
  'ADD_TODO': (state, action) => [...state, action.payload],
  'REMOVE_TODO': (state, action) => 
    state.filter(todo => todo !== action.payload)
});

柯里化的数学基础

柯里化源于数学中的柯里-霍华德同构(Curry-Howard correspondence),它展示了计算机程序与数学证明之间的深刻联系。在λ演算中,所有函数都只有一个参数,多参数函数通过柯里化实现:

λx.λy.λz.x + y + z

这对应于JavaScript中的:

const lambda = x => y => z => x + y + z;

柯里化的类型签名

在类型系统中,柯里化的类型转换可以表示为:

(a × b × c → d) → (a → b → c → d)

在TypeScript中:

type Curried<T, R> = 
  T extends [infer A, ...infer B] 
    ? (arg: A) => Curried<B, R> 
    : R;

function curry<T extends any[], R>(
  fn: (...args: T) => R
): Curried<T, R> {
  return function curried(...args: any[]): any {
    if (args.length >= fn.length) {
      return fn(...args as any);
    } else {
      return (...moreArgs: any[]) => curried(...args, ...moreArgs);
    }
  } as any;
}

const add = (a: number, b: number, c: number): number => a + b + c;
const curriedAdd = curry(add);
const result = curriedAdd(1)(2)(3); // 6

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步