Scala的库里化函数

重新看丢下的Scala语法,打算把Spark源码那部分再捡起来。之前翻看买的《函数式编程Scala》发现越来越跟我的初衷不同,我想全面细致重新再看一遍Scala的语法,而这书越来越集中于讲函数式编程的精神,于是只好退回来重新看《Scala编程》一书。

从Java程序员到Scala程序员,最大的一个转变就是从面向对象编程转换为函数式编程,所有方法都要求越来越简练,朝着函数命令看齐。Scala在抽象化和代码重用性上较之Java更上了一个层次,在Programming in Scala里有一个很好的例子理解Scala的抽象重用特点。


背景:需要一个api来搜索某个路径下文件名以特定查询值结尾的文件,可以这样写:

object FileMatcher {
  private def filesHere = 
  	(new java.io.File(".")).listFiles
  def filesEnding(query: String) =
    for (file <- filesHere; if file.getName.endsWith(query))
      yield file
}

既然有了查询以特定query结尾的方法,那也可以有文件名中包含某个query值的方法:

def filesContaining(query: String) =
  for (file <- filesHere; if file.getName.contains(query))
     yield file

或者更通用的关于文件名的查询方法:

def filesRegex(query: String) =
  for (file <- filesHere; if file.getName.matches(query))
    yield file

再进一步将匹配方法通用到更通用的method:

def filesMatching(query: String, method) =
   for (file <- filesHere; if file.getName.method(query))
      yield file

但是不能用方法名当做值传递,但是可以传递调用方法的函数值:

def filesMatching(query: String,
    matcher: (String, String) => Boolean) = {
  for (file <- filesHere; if matcher(file.getName, query))
    yield file
}

matcher方法表示传入两个String参数:file.getName和query,返回一个boolean值,有了这个方法,就能让三种不同的搜索方法调用,传入合适的函数来简化:

def filesEnding(query: String) =
  filesMatching(query, _.endsWith(_))
def filesContaining(query: String) =
  filesMatching(query, _.contains(_))
def filesRegex(query: String) =
  filesMatching(query, _.matches(_))

进一步简化,去除filesMatching和matcher方法中的query参数,尽量减少自由变量_存在的数量,而多一些绑定变量:

object FileMatcher {
  private def filesHere = (new java.io.File(".")).listFiles
  private def filesMatching(matcher: String => Boolean) =
    for (file <- filesHere; if matcher(file.getName))
      yield file
  def filesEnding(query: String) =
    filesMatching(_.endsWith(query))
  def filesContaining(query: String) =
    filesMatching(_.contains(query))
  def filesRegex(query: String) =
    filesMatching(_.matches(query))
}

Curry化的函数被应用了多个参数列表,而不仅仅是一个

未curry化:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (Int,Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3

curry化之后:代之以一个列表的两个Int参数

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (Int)(Int)Int
scala> curriedSum(1)(2)
res5: Int = 3

可以用占位符标注在curriedSum方法上:如

scala> val onePlus = curriedSum(1)_
onePlus: (Int) => Int = <function>

scala> onePlus(2)
res7: Int = 3

scala> val twoPlus = curriedSum(2)_
twoPlus: (Int) => Int = <function>

scala> twoPlus(2)
res8: Int = 4

例如现在有以下代码

 type IntPairPred = (Int, Int) => Boolean
 def sizeConstraint(pred: IntPairPred, n: Int, email: Email) =
      pred(email.text.size, n)

其中谓词函数IntpairPred接收一对整数(值n和Email的长度),检查n相对于Email长度之间的关系 比较的方法可以提前定义作为参数传入:

    val gt: IntPairPred = _ > _
    val ge: IntPairPred = _ >= _
    val lt: IntPairPred = _ < _
    val le: IntPairPred = _ <= _
    val eq: IntPairPred = _ == _

最后调用sizeConstraint方法,用IntPairPred传入第一个参数:

    val minimumSize: (Int, Email) => Boolean = sizeConstraint(ge, _: Int, _: Email)
    val maximumSize: (Int, Email) => Boolean = sizeConstraint(le, _: Int, _: Email)

对于没有传入的参数,需要用占位符_,并制定参数类型而且可以遗漏任意个、任意位置的参数 Scala里,可以用多个参数列表重新定义函数

    def sizeConstraint(pred: IntPairPred)(n: Int)(email: Email): Boolean =
      pred(email.text.size, n)
//可以变成一个可传递、可赋值的函数对象
    val sizeConstraintFn: IntPairPred => Int => Email => Boolean = sizeConstraint _

这种单参数的链式函数就是“库里化函数”,上面这个函数的意思是,sizeConstraintFn接受一个IntPairPred类型,返回一个函数,这个函数又接受一个Int类型,返回一个函数,最终这个函数接受一个Email类型,返回一个布尔值。现在可以把之前定义的IntPairPred参数传入函数,得到:

    val minSize: Int => Email => Boolean = sizeConstraint(ge)
    val maxSize: Int => Email => Boolean = sizeConstraint(le)

再传入Int参数,得到:

    val min20: Email => Boolean = minSize(20)
    val max20: Email => Boolean = maxSize(20)
Kaka Chen /
Published under (CC) BY-NC-SA in categories Scala  tagged with Scala