柯里化函数

什么是柯里化?

柯里化(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 中柯里化的主流实现方式。
相关推荐
毕设源码-邱学长1 分钟前
【开题答辩全过程】以 基于SpringBoot的理工学院学术档案管理系统为例,包含答辩的问题和答案
java·spring boot·后端
修己xj10 分钟前
SpringBoot解析.mdb文件实战指南
java·spring boot·后端
lpfasd12328 分钟前
Spring Boot 定时任务详解(从入门到实战)
spring boot·后端·python
moxiaoran575331 分钟前
Go语言的文件操作
开发语言·后端·golang
赴前尘1 小时前
记一次golang进程执行卡住的问题排查
开发语言·后端·golang
码农小卡拉1 小时前
Prometheus 监控 SpringBoot 应用完整教程
spring boot·后端·grafana·prometheus
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue球鞋购物系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
苏渡苇1 小时前
用 Spring Boot 项目给工厂装“遥控器”:一行 API 控制现场设备!
java·人工智能·spring boot·后端·网络协议·边缘计算
wangmengxxw2 小时前
设计模式 -详解
开发语言·javascript·设计模式
进击的小头2 小时前
设计模式落地的避坑指南(C语言版)
c语言·开发语言·设计模式