Scala基础 - 柯里化(Currying)及其应用

1. 介绍

柯里化(currying, 以逻辑学家Haskell Brooks Curry的名字命名)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。 在Scala中方法和函数有细微的差别,通常编译器会自动完成方法到函数的转换。如果想了解Scala方法和函数的具体区别,请参考博文Scala基础 - 函数和方法的区别

2. Scala中柯里化的形式

Scala中柯里化方法的定义形式和普通方法类似,区别在于柯里化方法拥有多组参数列表,每组参数用圆括号括起来,例如:

def multiply(x: Int)(y: Int): Int = x * y

multiply方法拥有两组参数,分别是(x: Int)和(y: Int)。 multiply方法对应的柯里化函数类型是:

Int => Int => Int

柯里化函数的类型声明是右结合的,即上面的类型等价于:

Int => (Int => Int)

表明该函数若只接受一个Int参数,则返回一个Int => Int类型的函数,这也和柯里化的过程相吻合。

3. 探究柯里化函数

我们仍以上面定义的multiply方法为例探索柯里化的一些细节:

def multiply(x: Int)(y: Int): Int = x * y

上面的代码定义了一个柯里化方法,在Scala中可以直接操纵函数,但是不能直接操纵方法,所以在使用柯里化方法前,需要将其转换成柯里化函数。最简单的方式是使用编译器提供的语法糖:

val f = multiply _

返回的函数类型是:

Int => Int => Int

使用Scala中的部分应用函数(partially applied function)技巧也可以实现转换,但是请注意转后后得到的并不是柯里化函数,而是一个接受两个(而不是两组)参数的普通函数:

val f = multiply(_: Int)(_: Int)

转后后得到的类型为:

(Int, Int) => Int

其实就是一个接受两个Int型参数的普通函数类型。

先传入第1个参数:

val f1 = f(1)

返回类型为:

Int => Int

即一个接受一个Int参数返回Int类型的函数。 继续传入第2个参数:

val f2 = f1(2)

返回类型为:

Int

两组参数都已经传入,返回一个Int类型结果。

4. 柯里化(currying)函数和部分应用函数(partial applied)的区别

下面代码定义一个普通方法multiply1和一个currying方法multiply2,并将其转换相应的函数类型:

def multiply1(x: Int, y:Int, z:Int) = x * y * z
val partialAppliedMultiply = multiply1 _
//类型:(x: Int, y: Int, z: Int) => Int

def multiply2(x: Int)(y: Int)(z: Int) = x * y * z
val curryingMultiply = multiply2 _
//类型:Int => (Int => (Int => Int))

在调用时,curryingMultiply可以依次传入各个参数,而partialAppliedMultiply在传入部分参数时,必须显示指定剩余参数的占位符:

val curryingMultiply1 = curryingMultiply(1)
//类型:Int => (Int => Int)

val partialAppliedMultiply1 = partialAppliedMultiply(1, _:Int, _: Int)
//类型:(Int, Int) => Int

另外,curryingMultiply1的类型仍然是currying类型,而partialAppliedMultiply1的类型仍然是普通函数类型。

5. 应用:控制抽象(Control Abstraction)

5.1 控制抽象介绍

对于一些通用的操作可以实现成控制抽象,例如像文件打开、关闭操作。实现成控制抽象的好处是,可以在使用的时候,看起来更像是语言级别提供的功能。

5.2 抽象控制的实现基础

5.2.1 无参函数

无参函数的类型是() => T,在使用时为了简化可以省略(),例如:

  def runInThread(block: => Unit){
    new Thread {
      override def run() { block }
    }.start()
  }

这样定义之后,在使用的时候就可以省略() =>,

  runInThread{
    println("Hi")
  }

5.2.2 使用{}替代()

如果方法只有一个参数,则可以使用{}替代(),例如:

  runInThread{
    println("Hi")
  }

5.2.3 传名参数(by-name parameter)

与传名参数相对的是传值参数。传值参数在函数调用之前表达式会被求值,例如Int,Long等数值参数类型;传名参数在函数调用前表达式不会被求值,而是会被包裹成一个匿名函数作为函数参数传递下去,例如高阶函数的函数参数就是传名参数。

5.3 控制抽象示例

withPrintWriter是一个柯里化方法,它接受两组参数,第1组参数是待操作的文件资源,第2组参数是操作文件资源的函数:

def withPrintWriter(file: File)(op: PrintWriter => Unit) {
    val writer = new PrintWriter(file)
    try {
      op(writer)
    } finally {
      writer.close()
    }
}

用法如下:

withPrintWriter(new File("date.txt")) {
    writer => writer.println(new java.util.Date)
}

withPrintWriter确保文件资源在被使用之后一定会被关闭,并且在使用的时候,看起来更像是语言内置的关键字函数。

6. 参考

  1. Programming in Scala, 2nd Edition
  2. 快学Scala

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Flutter入门

Kotlin中apply,run,let,also,with函数总结

run函数和apply函数很像,只不过run函数是使用最后一行的返回,apply返回当前自己的对象。

2732
来自专栏黑泽君的专栏

c语言基础学习07_关于指针的复习

=============================================================================

761
来自专栏塔奇克马敲代码

第6章 函数

2067
来自专栏纯洁的微笑

一个高频面试题,考考大家对 Java String 常量池的理解。

作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么,我们带着以下三个问题,去理解字符串...

2052
来自专栏吴裕超

总结一下js的原型和原型链

最近学习了js的面向对象编程,原型和原型链这块是个难点,理解的不是很透彻,这里搜集了一些这方面的资料,以备复习所用 一. 原型与构造函数   Js所有的函数都有...

3535
来自专栏架构之路

Java中Class类详解、用法及泛化

Java中Class类及用法 Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。这项信息纪录了每个对象所属的...

3086
来自专栏小樱的经验随笔

【Java学习笔记之十六】浅谈Java中的继承与多态

1、  什么是继承,继承的特点? 子类继承父类的特征和行为,使得子类具有父类的各种属性和方法。或子类从父类继承方法,使得子类具有父类相同的行为。 特点:在继承关...

2627
来自专栏学海无涯

19.Swift学习之构造函数与析构函数

1073
来自专栏派森公园

Scala中的闭包

除此之外,Scala还支持引用其他地方定义的变量:(x: Int) => x + more,这个函数将more也作为入参,不过这个参数是哪里来的?从这个函数的角...

561
来自专栏和蔼的张星的图像处理专栏

c++ primer2 变量和基本类型。

这四种初始化方式c++11都是支持的。c++11中用花括号来初始化变量得到了全面应用。

1191

扫码关注云+社区

领取腾讯云代金券