柯里化函数

什么是柯里化?

柯里化(Currying)是一种把"接收多个参数的函数",转换成 "一系列每次只接收一个参数的函数" 的技术。

  • 定义:将 f(a, b, c) 转化为 f(a)(b)(c)
  • 本质:利用闭包保存参数。
  • 作用:固定部分参数以复用代码、延迟计算、方便函数组合。

为什么要柯里化?

  • 柯里化的核心优势在于 参数复用 (让代码更具声明式特点,可读性更高) 和 延迟执行

    • 参数复用:
    javascript 复制代码
    // 假设有一个通用的正则验证函数 check(reg, txt)
    const check = reg => txt => reg.test(txt);
    
    // 我们可以生成具体的验证函数
    const checkEmail = check(/^\S+@\S+$/);
    const checkPhone = check(/^1\d{10}$/);
    
    // 以后使用时,只需要传具体的内容,逻辑非常清晰
    checkEmail('test@gmail.com'); 
    checkPhone('13800138000');
    • 延迟执行: 你不需要一次性集齐所有参数。你可以先传一部分参数,让程序"记住"这种状态,等到合适的时机(剩下的参数准备好时)再计算最终结果。
    • 函数组合: 在函数式编程中,我们经常需要把多个简单的函数组合成一个复杂的函数(比如 f(g(x)))。因为柯里化后的函数总是只接受一个参数,这使得它们非常容易像积木一样拼在一起。
  • 提高函数复用性

  • 更符合函数式编程(无状态、可组合、易测试)

  • 让代码语义更清晰

    js 复制代码
    request("GET")("/api/user")({ timeout: 1000 });

柯里化在 Haskell、Lisp 等纯函数式语言中是默认行为,Scala 也是语言层面原生支持,而在 JavaScript、Python 等语言中则是一种强大的编程技巧。

Scala(原生):参数列表天然支持多段、类型系统知道这是柯里化函数:

scala 复制代码
def add(a: Int)(b: Int) = a + b
add(1)(2)

Haskell(彻底原生):所有函数默认柯里化、多参数只是语法糖:

haskell 复制代码
add a b = a + b
add 1 2
(add 1) 2

Java 方法的柯里化

Java 不是原生支持柯里化的语言,依赖 Java 8 引入的 Lambda 表达式 和 函数式接口(Function)来模拟,间接实现。

由于 Java 可读性、类型冗长问题,柯里化不是 Java 的强项。由于语言层面的限制,Java 没有自动类型推导,像(Scala / Kotlin),没有函数是"一等公民"的完整体验,泛型 + Function 嵌套非常啰嗦,导致 Java 不"推崇"柯里化,Java 更推崇偏函数(准确说是偏应用),参考后续文章。Java 设计哲学偏:命令式、显式、可维护性 > 抽象美感。

不推荐:业务代码、CRUD、大多数后端服务逻辑使用柯里化,推荐:构建 DSL、高阶函数库(传入函数作为参数或返回函数的函数)、Stream / Pipeline 中的函数组合、需要"延迟绑定参数"使用柯里化。

java 复制代码
import java.util.function.Function;

public class LogDemo {
    public static void main(String[] args) {
        // 定义一个柯里化的日志函数
        // 第一层:接收日志级别 (level)
        // 第二层:接收具体消息 (message)
        Function<String, Function<String, String>> logFormatter = 
            level -> message -> "[" + level + "] " + message;

        // 场景:我们需要频繁打印 ERROR 级别的日志
        // 我们可以"固定"第一个参数,生成一个专门打印 Error 的函数
        Function<String, String> errorLogger = logFormatter.apply("ERROR");
        Function<String, String> infoLogger = logFormatter.apply("INFO");

        // 以后使用时,只需要传消息内容
        System.out.println(errorLogger.apply("数据库连接失败"));
        System.out.println(errorLogger.apply("空指针异常"));
        System.out.println(infoLogger.apply("系统启动成功"));
    }
}

如果参数多,类型定义会变得非常长且难看:

java 复制代码
Function<Integer,
    Function<Integer,
        Function<Integer, Integer>>> sum3 =
            a -> b -> c -> a + b + c;

int r = sum3.apply(1).apply(2).apply(3);

缺点(相比 JS 或 Haskell):

  • 啰嗦:类型签名非常长(Function<T, Function<...>>),可读性差。
  • 调用繁琐:必须显式调用 .apply(),不能像 JS 那样直接括号 func(a)(b),而是 func.apply(a).apply(b)
  • 性能:每次调用都会创建新的 Function 对象,在极端高性能敏感的场景下可能有微小的开销(通常可忽略)。

接下来看看我最喜欢的两门语言 Go 和 Kotlin 的柯里化。

Go 函数的柯里化

Go 语言没有原生的柯里化语法或机制,只能通过闭包和高阶函数来手动模拟。

普通手写柯里化版本:

比如一个 logger:

go 复制代码
func Log(level, module, msg string) {
    fmt.Println(level, module, msg)
}

柯里化它:

go 复制代码
func CurryLog(level string) func(string) func(string) {
    return func(module string) func(string) {
        return func(msg string) {
            fmt.Println(level, module, msg)
        }
    }
}

使用:

go 复制代码
info := CurryLog("INFO")
err  := CurryLog("ERROR")

dbInfo := info("DB")
dbErr  := err("DB")

dbInfo("connected")
dbInfo("query failed")
dbErr("add failed")
dbErr("update failed")

半通用转换器版本

写一个辅助函数,将任意符合特定签名的双参数函数转换为柯里化形式。

go 复制代码
package main

import "fmt"

// Curry 是一个通用转换器
// 它接收一个形式为 func(A, B) C 的函数
// 返回 func(A) func(B) C
func Curry[A, B, C any](f func(A, B) C) func(A) func(B) C {
    return func(a A) func(B) C {
        return func(b B) C {
            return f(a, b)
        }
    }
}

func Multiply(a, b int) int {
    return a * b
}

func main() {
    // 将普通函数转换为柯里化函数
    curriedMult := Curry(Multiply)

    // 创建一个"双倍器"
    doubler := curriedMult(2)

    fmt.Println(doubler(5))
    fmt.Println(doubler(10))
}

遗憾的是,Go 没有像 JS 那样的可变参数,不能柯里化任意长度参数列表的函数。

还有一个用泛型加反射实现的通用版本(不推荐)

go 复制代码
package curry

import "reflect"

func Curry(fn any) func(any) any {
    fnVal := reflect.ValueOf(fn)
    fnType := fnVal.Type()

    if fnType.Kind() != reflect.Func {
        panic("Curry expects a function")
    }

    arity := fnType.NumIn()

    var curry func(args []reflect.Value) func(any) any

    curry = func(args []reflect.Value) func(any) any {
        return func(arg any) any {
            newArgs := append(args, reflect.ValueOf(arg))

            if len(newArgs) < arity {
                return curry(newArgs)
            }

            // 参数齐了,调用函数
            results := fnVal.Call(newArgs)

            // 只支持 0 或 1 返回值
            if len(results) == 0 {
                return nil
            }
            return results[0].Interface()
        }
    }

    return curry(nil)
}

使用示例:

go 复制代码
func Add(a, b, c int) int {
    return a + b + c
}

func main() {
    c := Curry(Add)

    f1 := c(1).(func(any) any)
    f2 := f1(2).(func(any) any)
    result := f2(3).(int)

    fmt.Println(result)
}

支持不同类型参数:

go 复制代码
func Mix(a int, b string, c float64) string {
	return fmt.Sprintf("%d-%s-%.2f", a, b, c)
}

c := Curry(Mix)
r := c(1).(func(any) any)("go").(func(any) any)(3.14).(string)

⚠️ 注意:这个版本不优雅、不安全、不惯用,只能算"技术演示级别",不适合生产。技术上可实现(反射 + any),工程上不值得,Go 风格不推荐。

  1. 它完全丢失静态类型

    你看到的类型链是:

    go 复制代码
    func(any) any

    Go 编译器:

    • 无法检查参数类型
    • 无法检查返回类型
    • IDE 无法补全
  2. 性能很差

    • reflect.ValueOf
    • reflect.Call
    • interface{} 装箱/拆箱

    在热点路径上是灾难级别。

  3. 错误全在运行时爆炸

    go 复制代码
    c("hello") // panic

    不是编译期错误,而是 runtime panic。

柯里化在 Go 中的缺点

虽然能做,但在 Go 中滥用柯里化会被视为 非惯用做法,原因如下:

  1. 类型签名复杂

    • 普通:func(int, int, int) int
    • 柯里化:func(int) func(int) func(int) int
    • 这对阅读代码的人来说是折磨,而且 Go 不支持类型推导省略函数签名。
  2. 性能损耗

    每次调用柯里化函数都会涉及到内存分配(闭包对象)和函数调用开销。在高性能要求的场景(如紧密循环)中,这比直接传参要慢。

  3. 调试困难

    堆栈跟踪会因为层层嵌套的匿名函数变得很深且难以阅读。

为什么 Go 不原生支持柯里化?

这是 Go 的设计取向决定的:

Go 追求的是:

  • 显式
  • 简单
  • 可读
  • 易于静态分析和优化

而柯里化依赖:

  • 隐式闭包
  • 层层返回函数
  • 动态函数组合

这些在 Go 的设计哲学里被认为是可读性和可维护性成本过高

为什么泛型救不了 curry ?

Go 泛型不能:

  • 解构函数参数列表
  • 操作 variadic type list
  • 根据函数签名生成新函数类型

Go 没有 Type-Level Programming。

Go 里真正"惯用"的写法

Go 程序员更倾向写:

go 复制代码
func WithDB(db *DB) func(Request) Response {
    return func(req Request) Response {
        ...
    }
}

这是 Go 风格的依赖注入。

这本质就是:

柯里化 + 偏应用

只是 Go 不用这个词。

什么时候用?

  • 不要在简单的逻辑计算中使用(不要为了炫技而柯里化)。
  • 推荐使用场景:中间件设计、依赖注入、参数预设(偏函数应用)、接口适配器。

Kotlin 函数的柯里化

Kotlin 不原生支持自动柯里化(不像 Haskell),但语言特性非常适合手动柯里化,而且写法相当自然。

手动柯里化

kotlin 复制代码
// 普通多参数函数
fun sum(x: Int, y: Int, z: Int) = x + y + z

// 柯里化
fun sumCurried(x: Int) = { y: Int -> { z: Int -> x + y + z } }
// 带返回值写法
// fun sumCurried(x: Int): (Int) -> (Int) -> Int = { y -> { z -> x + y + z } }
// 或者函数表达式写法
// val sumCurried: (Int) -> (Int) -> Int = { x -> { y -> { z -> x + y + z } } }
// val sumCurried = { x: Int -> { y: Int -> { z: Int -> x + y + z } } }

// 调用
val sum1And = sumCurried(1)
val sum1And2And = sum1And(2)
var res = sum1And2And(3)

// 或者链式调用
res = sumCurried(1)(2)(3)
println(res)

通用柯里化扩展函数

泛型柯里化工具函数(非常实用)

kotlin 复制代码
fun <X, Y, Z, R> ((X, Y, Z) -> R).curry() = { x: X -> { y: Y -> { z: Z -> this(x, y, z) } } }

// 使用
val sum = { x: Int, y: Int, z: Int -> x + y + z }
val curriedSum = sum.curry()

val res = curriedSum(1)(2)(3)
println(res)

扩展:还有反柯里化函数:

kotlin 复制代码
fun <X, Y, Z, R> ((X) -> (Y) -> (Z) -> R).uncurry() = { x: X, y: Y, z: Z -> this(x)(y)(z) }

使用 Arrow 库柯里化 (推荐)

如果你在 Kotlin 中大量使用函数式编程,建议使用 Arrow 库(Kotlin 事实上的标准 FP 库)。它内置了强大的柯里化支持。

kotlin 复制代码
import arrow.core.curried

val add = { a: Int, b: Int -> a + b }
val curriedAdd = add.curried()

总结

  1. 柯里化将 (A, B) -> C 转化为 (A) -> (B) -> C。
  2. 实现方式:利用 Kotlin 的高阶函数,返回 Lambda 嵌套。
  3. 核心价值:实现参数复用和延迟执行,通过固定部分参数来创建更具体的函数(偏函数)。

JS 函数的柯里化

JavaScript 不是原生柯里化(Auto-curried)语言,在语言层面也没有像 Haskell 或 ML 那样"默认自动柯里化"的机制。 在 JavaScript 中,柯里化只是一种设计模式或编程技巧,而不是语言的默认行为。

虽然 JS 不是默认柯里化的,但现代 JavaScript (ES6+) 提供了很多语法糖,使得手动实现柯里化变得非常简便和优雅。只能说 JS 的动态类型和现代的简洁优雅的语法特性,使其特别容易实现柯里化。

A. 箭头函数

这是 JS 对柯里化最友好的语法支持。通过嵌套箭头函数,可以极其简洁地定义柯里化函数。

js 复制代码
// 现在的写法(极其接近数学定义)
const add = x => y => z => x + y + z;

// 使用
add(1)(2)(3);

B. Function.prototype.bind (偏函数应用)

虽然 bind 严格来说实现的是偏函数应用 (Partial Application),但它常被用来达到类似柯里化的效果(固定部分参数)。

js 复制代码
function add(x, y) {
    return x + y;
}

// 锁死第一个参数 x 为 10
const addTen = add.bind(null, 10);

console.log(addTen(5));

C. 通用柯里化转换函数实现自动柯里化

如果你想让普通的 JS 函数变成"不够参数就返回函数,够了就执行"的状态,需要写一个包装器:

js 复制代码
const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...more) => curried(...args, ...more);
  };
};

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

curriedAdd(1)(2)(3);
curriedAdd(1, 2)(3);
curriedAdd(1)(2, 3);

总结

  1. JS 不是原生柯里化语言:默认调用 f(a) 时,如果 f 需要两个参数,JS 不会返回函数,而是报错或计算出错误结果(NaN)。
  2. 没有专门的关键字:没有类似 curry function 这样的关键字。
  3. 语法支持不错:箭头函数 (a => b => c) 让手动写柯里化函数变得很轻松,这是目前 JS 中柯里化的主流实现方式。
相关推荐
JOEH602 小时前
🛡️ 微服务雪崩救星:Sentinel 限流熔断实战,3行代码搞定高可用!
后端·全栈
aiopencode2 小时前
iOS手动代码混淆函数和变量名基本原理和注意事项教程
后端
程序员威哥2 小时前
YOLOv8用ConvMixer结构:简化Backbone,速度+20%,mAP仅降0.9%
后端
开心猴爷3 小时前
如何在苹果手机上面进行抓包?iOS代理抓包,数据流抓包
后端
程序员威哥3 小时前
轻量型YOLO入门:在嵌入式设备上跑通目标检测(树莓派实战)
后端
程序员威哥3 小时前
基于YOLOv7的目标检测实战:彻底解决新手常见的「训练不收敛」问题
后端
程序员威哥3 小时前
从数据集标注到模型评估:YOLO完整工作流实战(附避坑清单)
后端
明月_清风3 小时前
模仿 create-vite / create-vue 风格写一个现代脚手架
前端·后端
南囝coding3 小时前
CSS终于能做瀑布流了!三行代码搞定,告别JavaScript布局
前端·后端·面试