深入理解Scala柯里化函数:函数链式变换

你写一个两数相加的函数:

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

Scala里还有另一种写法------把参数拆成两个列表:

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

这就是柯里化(Currying)。

很多人第一次看到这种写法,第一反应是:这不是多此一举吗?一个参数列表能写完的事,为什么要拆成两个?

这篇文章就来回答这个问题:柯里化到底有什么用?它的底层是什么?它和偏应用函数是什么关系?


先搞清楚:柯里化到底是什么

柯里化的定义:把一个接受多个参数的函数,变成一串只接受一个参数的函数。

scala 复制代码
// 普通函数:两个参数
def add(a: Int, b: Int): Int = a + b

// 柯里化:两个参数列表,每个列表一个参数
def addCurried(a: Int)(b: Int): Int = a + b

调用的时候:

scala 复制代码
add(1, 2)        // 3------一次传两个参数
addCurried(1)(2) // 3------传两次,每次一个参数

看起来只是写法不同------但底层完全不一样。

add(1, 2)是一次函数调用,传两个参数。

addCurried(1)(2)是两次函数调用------先传1,返回一个函数;再传2,得到结果。

scala 复制代码
// addCurried(1) 返回的是什么?------一个函数 Int => Int
val intermediate: Int => Int = addCurried(1)
intermediate(2)  // 3

看到了吗?addCurried(1)没有报"参数不够"------它返回了一个新的函数,这个函数记住了a=1,只等你传b。

柯里化的本质:多参数函数 = 单参数函数返回单参数函数。


柯里化的底层:它是高阶函数

柯里化函数在底层是什么?------它就是一个返回函数的高阶函数。

scala 复制代码
// 柯里化的写法
def addCurried(a: Int)(b: Int): Int = a + b

// 等价于这个返回函数的高阶函数
def addEquivalent(a: Int): Int => Int = {
  (b: Int) => a + b
}

这两个函数在底层几乎一模一样------都是先接受a,返回一个接受b的函数。

区别只是写法------def addCurried(a: Int)(b: Int)是语法糖,让你写起来更自然。

柯里化不是什么特殊的东西------它就是"返回函数的高阶函数"的语法糖。


柯里化到底有什么用?

这是最核心的问题。如果只是写法不同,为什么要多此一举?

用途1:天然支持偏应用

普通函数要偏应用,你得用_占位:

scala 复制代码
def add(a: Int, b: Int): Int = a + b
val addOne = add(1, _: Int)  // 要写_: Int

柯里化函数天然就是偏应用的------不用_:

scala 复制代码
def addCurried(a: Int)(b: Int): Int = a + b
val addOne = addCurried(1) _  // 自然偏应用
// 甚至在某些上下文里_都可以省

用途2:让函数参数更灵活------by-name参数和控制抽象

这是柯里化最有价值的用途------写出像语言内置语法一样的自定义控制结构。

scala 复制代码
// 自定义一个类似while的控制结构
def myWhile(condition: => Boolean)(body: => Unit): Unit = {
  if (condition) {
    body
    myWhile(condition)(body)
  }
}

var i = 0
myWhile(i < 5) {
  println(i)
  i += 1
}

注意最后那段------myWhile(i < 5) { ... }------花括号写在外面,看起来就像Scala内置的while语法。

为什么能这么写?------因为最后一个参数列表是一个函数参数,Scala允许你把函数参数用花括号写在外面(这叫尾置参数,trailing parameter)。

没有柯里化,你写不出这种像语法一样自然的控制结构。

用途3:类型推断更准确

Scala的类型推断是按参数列表进行的------前一个参数列表的类型,可以帮助推断后一个参数列表的类型。

scala 复制代码
// foldLeft的签名是柯里化的
def foldLeft[B](z: B)(op: (B, A) => B): B

// 调用的时候,第一个参数列表帮助推断第二个的类型
List(1,2,3).foldLeft(0)(_ + _)
// 第一个0推断B=Int,第二个参数就知道是(Int, Int) => Int

如果foldLeft不是柯里化的------foldLeft(z, op)------类型推断可能推断不出来,你就得手写类型。

用途4:隐式参数

Scala的implicit参数必须单独放在一个参数列表里------这天然就是柯里化的形式:

scala 复制代码
def foo(a: Int)(implicit b: String): String = a + b

Scala 3的using也是一样:

scala 复制代码
def foo(a: Int)(using ctx: Context): Result

柯里化和偏应用函数的关系

这两个概念经常被搞混。再说清楚一次:

  • 柯里化是函数的定义方式------把参数拆成多个列表
  • 偏应用是函数的使用方式------固定一部分参数,得到新函数

它们的关系:柯里化函数天然支持偏应用------因为每个参数列表本来就是"先传一部分"。

scala 复制代码
// 柯里化定义
def add(a: Int)(b: Int): Int = a + b

// 自然偏应用
val addOne = add(1) _

// 普通函数也能偏应用,但要用_
def add2(a: Int, b: Int): Int = a + b
val addOne2 = add2(1, _: Int)

简单说:柯里化让偏应用变得自然------不用写_: 类型。


几个常见的坑

坑1:不能省括号

scala 复制代码
addCurried(1)(2)  // 可以
addCurried(1, 2)  // 报错------柯里化是两个列表,不是一个列表两个参数

坑2:只传第一个列表会得到函数

scala 复制代码
val f = addCurried(1)
// f是Int => Int,不是Int------你还要再调用一次f(2)

很多人第一次用柯里化的时候,忘了传第二个参数列表,结果得到一个函数而不是值。

坑3:不要滥用柯里化

不是所有函数都该柯里化。如果你的参数总是一起传------没有"先传一部分"的场景------那就用普通参数列表。柯里化是为偏应用和控制抽象服务的,不是为了好看。


什么时候用柯里化?

场景 用柯里化吗?
需要偏应用------先固定一部分参数 用
写自定义控制结构(myWhile这类) 用
需要帮助类型推断(foldLeft这类) 用
有implicit/using参数 必须用
参数总是一次传完 不用
简单的普通函数 不用

判断标准:你有没有"参数分阶段传递"的需求? 有就柯里化,没有就普通写法。


写在最后

我写这篇文章的时候,特意没罗列柯里化有多少种写法------因为写法很简单。

真正值得理解的是:柯里化为什么是函数式编程的一个核心概念?

因为函数式编程里,函数是一等公民------函数可以返回函数。柯里化就是这个特性的直接体现:一个多参数函数,可以看成一串单参数函数的链。

这带来了什么?------参数可以分阶段传递。

你先传配置,得到一个"记住了配置"的函数;以后再传数据。你先传条件,得到一个控制结构;再传执行体。

Java里你做不到------Java的方法必须一次传完所有参数。你想"分阶段",只能用Builder模式,手动写一堆对象。

Scala用柯里化把这件事变成了语言层面的自然能力------不需要Builder,不需要工厂对象,函数本身就支持分阶段。

柯里化不是炫技------它是"函数可以返回函数"这个根本特性的语法表达。它让函数的组合、偏应用、控制抽象都变得自然。

当你写出foldLeft(0)(_ + _)、写出myWhile(cond) { body }的时候,你会明白:参数列表的边界,不只是参数的分组------它是函数变换的边界。

相关推荐
ShineWinsu41 分钟前
对于Qt:QTreeWidget常见属性以及QTreeWidgetItem类的解析
开发语言·c++·qt·gui·客户端·多叉树·qtreewidget
落羽的落羽1 小时前
【C++】C++17的主要特性
linux·服务器·开发语言·c++·算法·机器学习
总要学点什么2 小时前
第108篇:三维特效-图片轨迹线效果——把贴图滚成一串箭头,想用它的本色反而整条消失
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
starzy19906 小时前
深入理解Scala隐式值:编译期自动装配
开发语言·后端·scala
布莱克6056 小时前
Swift 条件语句详解:从 if 到 switch 的完整指南
开发语言·ssh·swift·switch·条件语句·guard·if
吴可可1237 小时前
ActiveX调用WScript.Shell原理揭秘
开发语言
晴天的雨.9928 小时前
【C++】继承篇
java·开发语言·c++
SomeOtherTime8 小时前
第 1 讲:内存布局与所有权解剖——栈帧、胖指针与“移动/拷贝”在汇编层面的真实面目
java·开发语言·汇编
总要学点什么8 小时前
第106篇:三维特效-流动线效果——让一颗光点沿整条线一路滚到头
开发语言·javascript·ecmascript·gis·webgl·材质·cesium