P.Pages

Programming notes / JavaScript × Go

柯里化

先得到函数,再得到结果。

从一行看不懂的代码,到一种可复用的思路。

一次传入f(a, b)
分步传入g(a)(b)
计算的目标不变,接收参数的方式变了。

01 / The idea

一次,只收一个参数

柯里化(Currying)把接收多个参数的函数,转换成一连串每次接收一个参数的函数。 最后一个参数传入后,才得到最终结果。定义 ↗

先看加法。左边的函数一次收齐两个数,右边先收第一个数,返回一个等待第二个数的函数。

// 普通形式:接收两个数,返回和
const add = (a, b) => a + b;
add(2, 3); // 5

// 柯里化形式:接收一个数,返回函数
const curriedAdd = (a) => (b) => a + b;
curriedAdd(2)(3); // 5

中间可以停下来,给返回的函数起个名字:

const add2 = curriedAdd(2); // 得到函数
add2(3); // 得到 5
add2(8); // 得到 10

02 / JavaScript

把两个箭头拆开看

下面这行的意思是:先给一个门槛,再给一个数,判断这个数是否达到门槛。min 是门槛,value 是待判断的数。

const atLeast = (min) => (value) => value >= min;

箭头函数可以写成 参数 => 表达式,表达式的值就是返回值。因此,给内层函数加上括号,就能看清谁被返回了:

const atLeast = min => (value => value >= min);
//             外层     └─ 返回的内层函数 ─┘

在这个例子里,展开成普通函数写法,计算行为相同:

function atLeast(min) {
  return function (value) {
    return value >= min;
  };
}
  1. 传入门槛const isAdult = atLeast(18);

    外层运行,min 是 18;返回内层函数,尚未执行比较。

  2. 保留函数isAdult

    它的判断逻辑相当于 value => value >= 18,可以反复调用。

  3. 传入数值isAdult(20); // true

    内层运行,value 是 20;计算 20 >= 18,返回 true。

为什么外层调用结束后,内层还能使用 min?因为闭包:函数可以继续访问它创建时所在作用域里的变量。这里没有修改 min,所以这个判断函数一直使用门槛 18。闭包 ↗

这里用普通函数展开,是为了看清结构。箭头函数与普通函数在 this 等语义上有区别,不能在所有场景中直接互换。箭头函数 ↗

03 / Go

返回类型,把答案写出来了

Go 没有 JavaScript 的箭头写法,但同样可以把函数作为返回值。普通版本返回 bool,柯里化版本先返回 func(int) bool。

// 普通形式
func atLeastPlain(min, value int) bool {
    return value >= min
}

// 柯里化形式
func atLeast(min int) func(int) bool {
    return func(value int) bool {
        return value >= min
    }
}

func atLeast(min int) func(int) bool

接收一个 int → 返回一个“接收 int、返回 bool”的函数。

下面是可直接保存为 main.go、用 go run main.go 运行的完整程序:

package main

import "fmt"

func atLeast(min int) func(int) bool {
    return func(value int) bool {
        return value >= min
    }
}

func main() {
    isAdult := atLeast(18)
    fmt.Println(isAdult(20))     // true
    fmt.Println(isAdult(16))     // false
    fmt.Println(atLeast(18)(20)) // true
}

Go 的函数字面量也是闭包。返回的函数仍然可以访问外层的 min,这一点与上面的 JavaScript 例子一致。Go 语言规范 ↗

04 / Why use it

把配置留住,把数据传进来

当很多次调用使用相同门槛时,可以先配置一次,再反复使用。通常把配置放在前面,把变化的数据放在后面,使用起来更顺手。

JavaScript:生成筛选条件

const atLeast = (min) => (value) => value >= min;
const isPassing = atLeast(60);

const scores = [45, 60, 72, 90];
scores.filter(isPassing); // [60, 72, 90]
scores.filter(atLeast(80)); // [90]

filter 需要一个判断函数。atLeast(60) 恰好生成了它,读起来就是“保留至少 60 分的分数”。不需要复用时,直接写 scores.filter(value => value >= 60) 也很清楚。

Go:在循环中复用判断函数

使用前面定义的 atLeast,把下面代码放进 main:

isPassing := atLeast(60)
scores := []int{45, 60, 72, 90}
passed := []int{}

for _, score := range scores {
    if isPassing(score) {
        passed = append(passed, score)
    }
}
fmt.Println(passed) // [60 72 90]

这里的收益是:给配置后的行为起名字,减少重复传参,并让函数更容易交给其他代码使用。 普通闭包也能实现这些目标;是否采用柯里化,取决于调用代码是否更清楚。

05 / Three different ideas

柯里化、部分应用、闭包

这三个概念经常一起出现,但各自回答不同的问题。柯里化与部分应用 ↗

柯里化 · 改变形式

把 f(a, b, c) 转换成 g(a)(b)(c):每层接收一个参数。

部分应用 · 固定参数

先传入一部分参数,得到接收剩余参数的函数。调用 atLeast(60) 就是在部分应用。

闭包 · 访问作用域
函数连同它能访问的词法环境,让返回的函数仍可使用外层变量。闭包本身不等于柯里化。

部分应用不要求先柯里化。例如,先固定 a,让返回的函数一次接收 b 和 c:

const sum3 = (a, b, c) => a + b + c;
const with10 = (b, c) => sum3(10, b, c);
with10(2, 3); // 15:部分应用,没有变成逐个传参

什么时候不用?

如果参数总是同时到齐,也没有复用中间函数的需求,add(2, 3) 通常更直接。柯里化不保证更快;多层函数也可能增加理解成本。尤其在 Go 中,要权衡额外的函数类型和嵌套是否值得。

检验一下:atLeast(60) 返回 true,还是一个函数?

返回一个函数。它还不知道待判断的数。再调用 atLeast(60)(60) 才得到 true ;调用 atLeast(60)(59) 得到 false。

06 / The name

为什么叫「柯里化」?

Currying 以数学家、逻辑学家 Haskell Curry(哈斯凯尔·柯里) 的姓氏命名。“柯里化”是对应的中文译名,所以单看名字,很难猜出它在做什么。命名来源 ↗

编程语言 Haskell 也以他命名:一个用了名字,一个用了姓氏。Haskell 报告序言 ↗

记住调用的两步就够了:


先给配置,得到函数。


再给数据,得到结果。

资料核对于 2026 年 9 月 28 日。文中示例为本页编写;链接指向语言文档与概念说明。