阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 函数柯里化

函数柯里化

作者:陈川 阅读数:33392人阅读 分类: JavaScript

函数柯里化的基本概念

函数柯里化是一种将多参数函数转换为一系列单参数函数的技术。它由数学家Haskell Curry命名,核心思想是把接受多个参数的函数变换成接受单一参数的函数,并返回接受余下参数的新函数。

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

// 柯里化后的加法函数
function curriedAdd(a) {
  return function(b) {
    return a + b
  }
}

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

柯里化的实现原理

实现柯里化的关键在于函数递归和参数收集。当传入的参数数量不足时,返回一个新函数继续收集参数;当参数数量足够时,执行原始函数。

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)
console.log(curriedSum(1)(2)(3)) // 6
console.log(curriedSum(1, 2)(3)) // 6
console.log(curriedSum(1, 2, 3)) // 6

柯里化的高级应用

柯里化不仅仅是参数转换,它还能实现函数组合和延迟执行等高级功能。

参数复用

// 创建特定前缀的日志函数
function log(level, message) {
  console.log(`[${level}] ${message}`)
}

const curriedLog = curry(log)
const debugLog = curriedLog('DEBUG')
const errorLog = curriedLog('ERROR')

debugLog('This is a debug message') // [DEBUG] This is a debug message
errorLog('Something went wrong!') // [ERROR] Something went wrong!

函数组合

function compose(...fns) {
  return fns.reduce((f, g) => (...args) => f(g(...args)))
}

const toUpper = str => str.toUpperCase()
const exclaim = str => `${str}!`
const shout = compose(exclaim, toUpper)

console.log(shout('hello')) // "HELLO!"

// 柯里化版本
const curriedCompose = (...fns) => x => fns.reduceRight((v, f) => f(v), x)
const shout2 = curriedCompose(exclaim, toUpper)
console.log(shout2('world')) // "WORLD!"

柯里化的实际应用场景

事件处理

// 普通事件处理
document.querySelector('#btn').addEventListener('click', (event) => {
  sendAnalytics('click', event.target.id)
})

// 柯里化版本
const sendEvent = curry((type, id, event) => {
  sendAnalytics(type, id)
})

document.querySelector('#btn').addEventListener('click', sendEvent('click', 'btn'))

API请求

// 普通API请求
function fetchApi(method, endpoint, data) {
  return fetch(endpoint, {
    method,
    body: JSON.stringify(data)
  })
}

// 柯里化版本
const curriedFetch = curry(fetchApi)
const get = curriedFetch('GET')
const post = curriedFetch('POST')

const getUsers = get('/api/users')
const createUser = post('/api/users')

// 使用
getUsers().then(/* ... */)
createUser({ name: 'John' }).then(/* ... */)

柯里化的性能考量

虽然柯里化提供了很多便利,但也需要考虑其性能影响:

  1. 每次柯里化都会创建新的函数对象,可能增加内存消耗
  2. 深层嵌套的柯里化调用可能导致调用栈增加
  3. 在性能敏感的场景中需要谨慎使用
// 性能测试示例
function testPerformance() {
  const normalAdd = (a, b, c, d) => a + b + c + d
  const curriedAdd = curry(normalAdd)
  
  console.time('normal')
  for (let i = 0; i < 1000000; i++) {
    normalAdd(1, 2, 3, 4)
  }
  console.timeEnd('normal')
  
  console.time('curried')
  for (let i = 0; i < 1000000; i++) {
    curriedAdd(1)(2)(3)(4)
  }
  console.timeEnd('curried')
}

testPerformance()
// 正常调用通常比柯里化调用快2-5倍

柯里化与部分应用的比较

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

  • 柯里化:将多参数函数转换为嵌套的单参数函数
  • 部分应用:固定一个函数的部分参数,产生一个更小元的函数
// 部分应用实现
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs)
  }
}

// 使用示例
function greet(greeting, name, punctuation) {
  return `${greeting}, ${name}${punctuation}`
}

const sayHello = partial(greet, 'Hello')
const sayHelloToJohn = partial(greet, 'Hello', 'John')

console.log(sayHello('John', '!')) // "Hello, John!"
console.log(sayHelloToJohn('!')) // "Hello, John!"

在函数式编程库中的应用

主流函数式编程库如Ramda和Lodash都提供了完善的柯里化支持。

Ramda的自动柯里化

import R from 'ramda'

const addFourNumbers = (a, b, c, d) => a + b + c + d
const curriedAdd = R.curry(addFourNumbers)

const f = curriedAdd(1, 2)
const g = f(3)
g(4) // 10

Lodash的柯里化

import _ from 'lodash'

function greet(greeting, name) {
  return greeting + ' ' + name
}

const curriedGreet = _.curry(greet)
const sayHello = curriedGreet('Hello')
console.log(sayHello('Alice')) // "Hello Alice"

柯里化的变体实现

除了标准柯里化外,还有一些变体实现方式:

无限参数柯里化

function infiniteCurry(fn) {
  return function curried(...args) {
    if (args.length === 0) {
      return curried
    }
    return (...args2) => {
      if (args2.length === 0) {
        return fn(...args)
      }
      return curried(...args, ...args2)
    }
  }
}

const sum = infiniteCurry((...nums) => nums.reduce((a, b) => a + b, 0))
console.log(sum(1)(2)(3)()) // 6
console.log(sum(1, 2)(3, 4)(5)()) // 15

占位符柯里化

function curryWithPlaceholder(fn) {
  return function curried(...args) {
    const complete = args.length >= fn.length && !args.includes(curryWithPlaceholder._)
    if (complete) {
      return fn.apply(this, args)
    }
    return function(...args2) {
      const combined = []
      let i = 0
      for (const arg of args) {
        combined.push(arg === curryWithPlaceholder._ && i < args2.length ? args2[i++] : arg)
      }
      while (i < args2.length) {
        combined.push(args2[i++])
      }
      return curried.apply(this, combined)
    }
  }
}

curryWithPlaceholder._ = Symbol('placeholder')

// 使用示例
const curriedFn = curryWithPlaceholder(function(a, b, c, d) {
  return [a, b, c, d]
})

const _ = curryWithPlaceholder._
console.log(curriedFn(1)(2)(3)(4)) // [1, 2, 3, 4]
console.log(curriedFn(_, 2)(1, _, 4)(3)) // [1, 2, 3, 4]

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:函数属性和方法

下一篇:对象创建方式

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌