Scala学习笔记 & 一些不错的学习材料 & 函数编程的历史八卦

参考这篇文章:

http://www.ibm.com/developerworks/cn/java/j-lo-funinscala1/

这也是一个系列

严格意义上的编程范式分为:命令式编程(Imperative Programming)、函数式编程(Functional Programming)和逻辑式编程(Logic Programming)

使用递归的方式去思考

清单 1. 数列求和

   //xs.head 返回列表里的头元素,即第一个元素
   //xs.tail 返回除头元素外的剩余元素组成的列表
  def sum(xs: List[Int]): Int = 
 if (xs.isEmpty) 0 else xs.head + sum(xs.tail)
清单 2. 求最大值
def max(xs: List[Int]): Int = { 
    if (xs.isEmpty) 
      throw new java.util.NoSuchElementException 
    if (xs.size == 1) 
      xs.head 
    else 
      if (xs.head > max(xs.tail)) xs.head else max(xs.tail) 
 }
清单 3. 反转字符串
def reverse(xs: String): String = 
 if (xs.length == 1) xs else reverse(xs.tail) + xs.head
清单 4. 快速排序
 def quickSort(xs: List[Int]): List[Int] = { 
    if (xs.isEmpty) xs 
    else 
      quickSort(xs.filter(x=>x<xs.head)):::xs.head::quickSort(xs.filter(x=>x>xs.head)) 
 }

习惯于命令式编程范式的程序员还有一个担忧:相比循环,递归不是存在效率问题吗?每一次递归调用,都会分配一个新的函数栈,如果递归嵌套很深,容易出现栈溢出的问题。比如下面计算阶乘的递归程序:

清单 5. 递归求阶乘
def factorial(n: Int): Int = 
 if (n == 0) 1 else n * factorial(n - 1)

当 n很大时,函数栈将很快被耗尽。然而尾递归能帮我们解决这个问题,所谓尾递归是指在函数调用的最后一步,只调用该递归函数本身,此时,由于无需记住其他变量,当前的函数栈可以被重复使用。上面的程序只需稍微改造一下,既可以变成尾递归式的程序,在效率上,和循环是等价的。

注:C++编译器对尾递归也进行了优化,但是编译器选项默认不打开,需要主动打开;Java没有对尾递归做优化。

清单 6. 尾递归求阶乘
def factorial(n: Int): Int = { 
    @tailrec 
    def loop(acc: Int, n: Int): Int = 
      if (n == 0) acc else loop(n * acc, n - 1) 

    loop(1, n) 
 }

在上面的程序中,我们在阶乘函数内部定义了一个新的递归函数,该函数最后一步要么返回结果,要么调用该递归函数本身,所以这是一个尾递归函数。该函数多出一个变量 acc,每次递归调用都会更新该变量,直到递归边界条件满足时返回该值,即为最后的计算结果。这是一种通用的将非尾递归函数转化为尾递归函数的方法,大家可多加练习,掌握这一方法。对于尾递归,Scala 语言特别增加了一个注释 @tailrec,该注释可以确保程序员写出的程序是正确的尾递归程序,如果由于疏忽大意,写出的不是一个尾递归程序,则编译器会报告一个编译错误,提醒程序员修改自己的代码。

把上面的解法写进程序里就是:

package com.spark.my

import scala.annotation.tailrec

object Hello {
  def main(args: Array[String]): Unit = {
    println("hello world")
    val ret = factorial(3)
    println("ret: %d".format(ret))
  }

  def factorial(n: Int): Int = {
    @tailrec
    def loop(acc: Int, n: Int): Int =
      if (n == 0) acc else loop(n * acc, n - 1)

    loop(1, n)
  }

}

运行结果:

hello world
ret: 6

Process finished with exit code 0

一道面试题

也许有的读者看了上面的例子后,还是感到不能信服:虽然使用递归会让程序变得简洁易懂,但我用循环也一样可以实现,大不了多几行代码而已,而且我还不用知道什么尾递归,写出的程序就是效率最高的。那我们一起来看看下面这个问题:有趣的零钱兑换问题。题目大致如下:

假设某国的货币有若干面值,现给一张大面值的货币要兑换成零钱,问有多少种兑换方式。

解答:第一种找零方式总共有 countChange(money, coins.tail)种,第二种找零方式等价为对于 money – conins.head进行同样的兑换,则这种兑换方式有 countChange(money - coins.head, coins)种,两者之和即为所有的零钱兑换方式。

写成代码:

package com.spark.my

import scala.annotation.tailrec

object Hello {
  def main(args: Array[String]): Unit = {
    println("hello world")
    val coins = List(1, 2, 5)
    val money = 10
    val ret = count(money, coins)
    println("ret: %d".format(ret))
  }

  def count(money: Int, coins: List[Int]): Int = {
    if (money == 0)
      1
    else if (coins.size == 0 || money < 0)
      0
    else
      count(money, coins.tail) + count(money - coins.head, coins)
  }

}

运行:

hello world
ret: 10

Process finished with exit code 0

但是注意,开始的时候,我在count前面加上了 @tailrec,但是报错,说这个不是尾递归。还不清楚,怎么把这个改成尾递归。

结束语

事实上,在 Haskell 语言中,不存在 while、for 等命令式编程语言中必不可少的循环控制语句,Haskell 强迫程序员使用递归等函数式编程的思维去解决问题。作者也鼓励大家以后碰到问题时,先考虑有没有好的递归的方式实现,看看是否会为我们关于编程的理解带来新的思考。

而Scala融合了Functional programming 和 OO programming,有class和Object.

开始看第二篇了:

http://www.ibm.com/developerworks/cn/java/j-lo-funinscala2/

Scala 语法非常简洁,拥有其他语言编程经验的程序员很容易读懂 Scala 代码。现在我们将回过头来,从基本的语法开始学习 Scala 语言。大家会发现 Scala 语言异常精炼,实现同样功能的程序,在代码量上,使用 Scala 实现通常比 Java 实现少一半或者更多。短小精悍的代码常常意味着更易懂,更易维护。本文将为大家介绍 Scala 语言的基本语法,帮助大家写出自己的第一个 Scala 程序。

Scala 为定义变量提供了两种语法。

使用 val定义常量,一经定义后,该变量名不能被重新赋值。

使用 var定义变量,可被重新赋值。

在 Scala 中,鼓励使用 val,除非你有明确的需求使用 var。对于 Java 程序员来说,刚开始可能会觉得有违直觉,但习惯后你会发现,大多数场合下我们都不需要 var,一个可变的变量。

清单 1. 定义变量
  val x = 0 
  var y = 1 
  
  y = 2 
  // 给常量赋值会出现编译错误
  // x = 3 

  // 显式指定变量类型
  val x1: Int = 0 
  var y1: Int = 0

定义变量时没有指定变量类型。这是否意味着 Scala 是和 Python 或者 Ruby 一样的动态类型语言呢?

恰恰相反,Scala 是严格意义上的静态类型语言,由于其采用了先进的类型推断(Type Inference)技术,程序员不需要在写程序时显式指定类型,编译器会根据上下文推断出类型信息。比如变量 x被赋值为 0,0 是一个整型,所以 x的类型被推断出为整型。当然,Scala 语言也允许显示指定类型,如变量 x1y1的定义。一般情况下,我们应尽量使用 Scala 提供的类型推断系统使代码看上去更加简洁。

函数的定义也非常简单,使用关键字 def,后跟函数名和参数列表,如果不是递归函数可以选择省略函数返回类型。

Scala 还支持定义匿名函数,匿名函数由参数列表,箭头连接符和函数体组成。

清单 2. 定义函数
  // 定义函数
  def square(x: Int): Int = 
    x * x                      
  // 如果不是递归函数,函数返回类型可省略
  def sum_of_square(x: Int, y: Int) = 
    square(x) + square(y)  

  sum_of_square(2, 3)   
  
  // 定义匿名函数
  val cube = (x: Int) => x * x *x 
  cube(3) 

  // 使用匿名函数,返回列表中的正数
  List(-2, -1, 0, 1, 2, 3).filter(x => x > 0)

Scala 中的一条语句其实是一个表达式,函数的执行过程就是对函数体内的表达式的求值过程,最后一条表达式的值就是函数的返回值。如果函数体只包含一条表达式,则可以省略 {}。其次,没有显式的 return语句,最后一条表达式的值会自动返回给函数的调用者。

和 Java 不同,在 Scala 中,函数内部还可以定义其他函数。比如上面的程序中,如果用户只对 sum_of_square 函数感兴趣,则我们可以将 square 函数定义为内部函数,实现细节的隐藏。

清单 3. 定义内部函数
def sum_of_square(x: Int, y: Int): Int = { 
    def square(x: Int) = 
      x * x 
    square(x) + square(y) 
  }

流程控制语句

和 Java 中对应的条件判断语句不同,Scala 中的 if else是一个表达式,根据条件的不同返回相应分支上的值。比如下面例子中求绝对值的程序,由于 Scala 中的 if else是一个表达式,所以不用像 Java 那样显式使用 return返回相应的值。

清单 4. 使用 if else 表达式
def abs(n: Int): Int = 
 if (n > 0) n else -n

和 Java 一样,Scala 提供了用于循环的 while 语句,在下面的例子中,我们将借助 while 循环为整数列表求和。

清单 5. 使用 while 为列表求和
def sum(xs: List[Int]) = { 
    var total = 0 
    var index = 0 
    while (index < xs.size) { 
      total += xs(index) 
      index += 1 
    } 
    total 
  }

上述程序是习惯了 Java 或 C++ 的程序员想到的第一方案,但仔细观察会发现有几个问题:

首先,使用了 var定义变量,我们在前面说过,尽量避免使用 var

其次,这个程序太长了,第一次拿到这个程序的人需要对着程序仔细端详一会:程序首先定义了两个变量,并将其初始化为 0,然后在 index小于列表长度时执行循环,在循环体中,累加列表中的元素,并将 index加 1,最后返回最终的累加值。

直到这时,这个人才意识到这个程序是对一个数列求和。

让我们换个角度,尝试用递归的方式去思考这个问题,对一个数列的求和问题可以简化为该数列的第一个元素加上由后续元素组成的数列的和,依此类推,直到后续元素组成的数列为空返回 0。具体程序如下,使用递归,原来需要 9 行实现的程序现在只需要两行,而且程序逻辑看起来更清晰,更易懂。(关于如何使用递归的方式去思考问题,请参考作者的另外一篇文章《使用递归的方式去思考》)(已在本文的前半部分学习并引用)

清单 6. 使用递归对数列求和
//xs.head 返回列表里的头元素,即第一个元素
//xs.tail 返回除头元素外的剩余元素组成的列表
  def sum1(xs: List[Int]): Int = 
 if (xs.isEmpty) 0 else xs.head + sum1(xs.tail)

有没有更简便的方式呢?答案是肯定的,我们可以使用列表内置的一些方法达到同样的效果:

xs.foldLeft(0)((x0, x) => x0 + x)

该方法传入一个初始值 0,一个匿名函数,该匿名函数累加列表中的每一个元素,最终返回整个列表的和。使用上面的方法,我们甚至不需要定义额外的方法,就可以完成同样的操作。

事实上,List 已经为我们提供了 sum 方法,在实际应用中,我们应该使用该方法,而不是自己定义一个。作者只是希望通过上述例子,让大家意识到 Scala 虽然提供了用于循环的 while 语句,但大多数情况下,我们有其他更简便的方式能够达到同样的效果。

使用牛顿法求解平方根

可假设其平方根为任意一个正数 ( 在这里,我们选定 1 为初始的假设 ),然后比较 x与该数的平方,如果两者足够近似(比如两者的差值小于 0.0001),则该正数即为 x的平方根;否则重新调整假设,假设新的平方根为 上次假设与 x/ 上次假设的和的平均数。通过下表可以看到,经过仅仅 4 次迭代,就能求解出相当精确的 2 的平方根。

将上述算法转化为 Scala 程序,首先我们定义这个迭代过程,这也是该算法的核心部分,所幸这一算法非常简单,利用递归,一个 if else表达式就能搞定。后续为两个辅助方法,让我们的程序看起来更加清晰。最后我们选定初始假设为 1,定义出最终的 sqrt方法。

代码如下:

package com.spark.my

import scala.annotation.tailrec

object Hello {
  def main(args: Array[String]): Unit = {
    // 测试代码
    val ret = sqrt(2)
    println(ret)
  }

  // 迭代函数,若解不满足精度,通过递归调用接续迭代
  def sqrtIter(guess: Double, x: Double): Double =
  if (isGoodEnough(guess, x))
    guess
  else
    sqrtIter((guess + x / guess)/2, x)

  // 判断解是否满足要求
  def isGoodEnough(guess: Double, x: Double) =
  abs(guess * guess - x)< 0.0001


  // 辅助函数,求绝对值
  def abs(x: Double) =
  if (x < 0) -x else x

  // 目标函数
  def sqrt(x: Double): Double =
  sqrtIter(1, x)


}

然而这段程序也有一个显而易见的缺陷,作为用户,他们只关心 sqrt函数,但这段程序却将其他一些辅助函数也暴露给了用户,我们在前面提到过,Scala 里可以嵌套定义函数,我们可以将这些辅助函数定义为 sqrt的内部函数,更进一步,由于内部函数可以访问其函数体外部定义的变量,我们可以去掉这些辅助函数中的 x参数。最终的程序如下:

package com.spark.my

import scala.annotation.tailrec

object Hello {
  def main(args: Array[String]): Unit = {
    // 测试代码
    val ret = sqrt(3)
    println(ret)
  }

  // 目标函数,通过将需要用到的辅助函数定义为内部函数,实现细节的隐藏
  def sqrt(x: Double): Double = {

    // 迭代函数,若解不满足精度,通过递归调用接续迭代
    def sqrtIter(guess: Double): Double =
    if (isGoodEnough(guess))
      guess
    else
      sqrtIter((guess + x / guess) / 2)

    // 判断解是否满足要求
    def isGoodEnough(guess: Double) =
    abs(guess * guess - x) < 0.0001

    // 辅助函数,求绝对值
    def abs(x: Double) =
    if (x < 0) -x else x

    sqrtIter(1)
  }

}

 

如何运行 Scala 程序

作为应用程序执行

作为应用程序执行时,我们需要在一个单例对象中定义入口函数 main,经过编译后就可以执行该应用程序了。

清单 10. HelloWorld.scala
object HelloWorld { 
  def main(args: Array[String]): Unit = { 
    println("Hello World!") 
  } 
 }

Scala 还提供了一个更简便的方式,直接继承另一个对象 App,无需定义 main方法,编译即可运行。

清单 11. HelloScala.scala
object HelloScala extends App { 
  println("Hello Scala!") 
 }

亲测,可用。

结束语

本文为大家介绍了 Scala 的基本语法,相比 Java,Scala 的语法更加简洁,比如 Scala 的类型推断可以省略程序中绝大多数的类型声明,短小精悍的匿名函数可以方便的在函数之间传递,还有各种在 Scala 社区约定俗成的习惯,比如省略的分号以及函数体只有一条表达式时的花括号,这一切都帮助程序员写出更简洁,更优雅的程序。限于篇幅,本文只介绍了 Scala 最基本的语法,如果读者想跟进一步学习 Scala,请参考 Scala 的 官方文档及文末所附的参考资源。

掌握了这些基本的语法,我们将在下一篇文章中为大家介绍如何使用 Scala 进行函数式编程,这是 Scala 最让人心动的特性之一,对于习惯了面向对象的程序员来说,学习 Scala 更多的是在学习如何使用 Scala 进行函数式编程。

参考资料

学习

开始学习这篇文章

http://www.ibm.com/developerworks/cn/java/j-lo-funinscala3/index.html

即使你是一个刚刚踏入职场的新人,如果在面试时能有意无意地透露出你懂那么一点点函数式编程,也会让你的面试官眼前一亮。

阿兰·图灵(Alan Turing)和约翰·冯·诺伊曼(John von Neumann)。阿兰·图灵提出了图灵机的概念,约翰·冯·诺伊曼基于这一理论,设计出了第一台现代计算机。由于图灵以及冯·诺伊曼式计算机的大获成功,历史差点淹没了另外一位同样杰出的科学家和他的理论,那就是阿隆佐·邱奇(Alonzo Church)和他的λ演算。

阿隆佐·邱奇是阿兰·图灵的老师,上世纪三十年代,他们一起在普林斯顿研究可计算性问题,为了回答这一问题,阿隆佐·邱奇提出了λ演算,其后不久,阿兰·图灵提出了图灵机的概念,尽管形式不同,但后来证明,两个理论在功能上是等价的,条条大路通罗马。

如果不是约翰·麦卡锡(John McCarthy),阿隆佐·邱奇的λ演算恐怕还要在历史的故纸堆中再多躺几十年,约翰·麦卡锡是人工智能科学的奠基人之一,他发现了λ演算的珍贵价值,发明了基于λ演算的函数式编程语言:Lisp,由于其强大的表达能力,一推出就受到学术界的热烈欢迎,以至于一段时间内,Lisp 成了人工智能领域的标准编程语言。

很快,λ演算在学术界流行开来,出现了很多函数式编程语言:Scheme 、SML、Ocaml 等,但是在工业界,还是命令式编程语言的天下,Fortran、C、C++、Java 等。

随着时间的流逝,越来越多的计算机从业人员认识到函数式编程的意义,爱立信公司于上世纪八十年代开发出了 Erlang 语言来解决并发编程的问题;在互联网的发展浪潮中,越来越多的语言也开始支持函数式编程:JavaScript、Python、Ruby、Haskell、Scala 等。可以预见,如果过去找一个懂什么是函数式编程的程序员很困难,那么在不久的将来,找一个一点也没听过函数式编程的程序员将更加困难。

什么是函数式编程

狭义地说,函数式编程没有可变的变量、循环等这些命令式编程方式中的元素,像数学里的函数一样,对于给定的输入,不管你调用该函数多少次,永远返回同样的结果。而在我们常用的命令式编程方式中,变量用来描述事物的状态,整个程序,不过是根据不断变化的条件来维护这些变量。

广义地说,函数式编程重点在函数,函数是这个世界里的一等公民,函数和其他值一样,可以到处被定义,可以作为参数传入另一个函数,也可以作为函数的返回值,返回给调用者。利用这些特性,可以灵活组合已有函数形成新的函数,可以在更高层次上对问题进行抽象。本文的重点将放在这一部分。

函数式编程有什么优点

约翰·巴克斯(John Backus)为人熟知的两项成就是 FORTRAN 语言和用于描述形式系统的巴克斯范式,因为这两项成就,他获得了 1977 年的图灵奖。

有趣的是他在获奖后,做了一个关于函数式编程的讲演:Can Programming Be Liberated From the von Neumann Style? 1977 Turing Award Lecture。他认为像 FORTRAN 这样的命令式语言不够高级,应该有新的,更高级的语言可以摆脱冯诺依曼模型的限制,于是他又发明了 FP 语言,虽然这个语言未获成功,但是约翰·巴克斯关于函数式编程的论述却得到了越来越多的认可。

下面,我们就罗列一些函数式编程的优点。

首先,函数式编程天然有并发的优势。由于工艺限制,摩尔定律已经失效,芯片厂商只能采取多核策略。程序要利用多核运算,必须采取并发,而并发最头疼的问题就是共享数据,狭义的函数式编程没有可变的变量,数据只读不写,并发的问题迎刃而解。这也是前面两篇文章中,一直建议大家在定义变量时,使用 val 而不是 var 的原因。爱立信公司发明的 Erlang 语言就是为解决并发的问题而生,在电信行业取得了不俗的成绩。

其次,函数式编程有迹可寻。由于不依赖外部变量,给定输入函数的返回结果永远不变,对于复杂的程序,我们可以用值替换的方式(substitution model)化繁为简,轻松得出一段程序的计算结果。为这样的程序写单元测试也很方便,因为不用担心环境的影响。

最后,函数式编程高屋建瓴。写程序最重要的就是抽象,不同风格的编程语言为我们提供了不同的抽象层次,抽象层次越高,表达问题越简洁,越优雅。读者从下面的例子中可以看到,使用函数式编程,有一种高屋建瓴的感觉。

抽象,抽象,再抽象!

清单 2. 求立方函数
   def cube(n: Int) = n * n * n 

   cube(35) 
   cube(68) 
清单 3. 求立方和
def cube(n: Int) = n * n * n 
def sumCube(a: Int, b: Int): Int = 
 if (a > b) 0 else cube(a) + sumCube(a + 1, b) 
sumCube(1, 10)

是时候教给他第二项本领了:高阶函数(Higher-Order Function),所谓高阶函数,就是操作其他函数的函数。以求和为例,我们可以定义一个新的求和函数,该函数接受另外一个函数作为参数,这个作为参数的函数代表了某种对数据的操作。使用高阶函数后,抽象层次提高,代码变得更简单了。

   def cube(n: Int) = n * n * n 
   def id(n: Int) = n 
   def square(n : Int) = n * n 
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高阶函数
   def sum(f: Int => Int, a: Int, b: Int): Int = 
    if (a > b) 0 else f(a) + sum(f, a + 1, b) 

   // 使用高阶函数重新定义求和函数
   def sumCube(a: Int, b: Int): Int = sum(cube, a, b) 
   def sumSquare(a: Int, b: Int): Int = sum(square, a, b) 
   def sumFact(a: Int, b: Int): Int = sum(fact, a, b) 
   def sumInt(a: Int, b: Int): Int = sum(id, a, b) 

   // do it
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

对于简单的函数,我们还可以将其转化为匿名函数,让程序变得更简洁一些。在高阶函数中使用匿名函数,这是函数式编程中经常用到的一个技巧,多数情况下,我们关心的是高阶函数,而不是作为参数传入的函数,所以为其单独定义一个函数是没有必要的。

值得称赞的是 Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体,参数的类型是可省略的,Scala 的类型推测系统会推测出参数的类型。使用匿名函数后,我们的代码变得更简洁了:

清单 6. 在高阶函数中使用匿名函数
def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高阶函数
   def sum(f: Int => Int, a: Int, b: Int): Int = 
    if (a > b) 0 else f(a) + sum(f, a + 1, b) 

   // 使用高阶函数重新定义求和函数
   def sumCube(a: Int, b: Int): Int = sum(x => x * x * x, a, b) 
   def sumSquare(a: Int, b: Int): Int = sum(x => x * x, a, b) 
   def sumFact(a: Int, b: Int): Int = sum(fact, a, b) 
   def sumInt(a: Int, b: Int): Int = sum(x => x, a, b) 

   // 有了这些函数,小龙做起作业轻松多了
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

 

柯里化

上面使用匿名函数后的高阶函数还有什么地方值得改进呢?希望大家还会想起那句话:Don ’ t Repeat Yourself !求和函数的两个上下限参数 a,b被重复得传来传去。我们试着重新定义 sum函数,让它接受一个函数作为参数,同时返回另外一个函数。看到没?使用新的 sum函数,我们再定义各种求和函数时,完全不需要这两个上下限参数了,我们的程序又一次得到了简化。

清单 7. 返回函数的高阶函数
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高阶函数
   def sum(f: Int => Int): (Int, Int) => Int = { 
    def sumF(a: Int, b: Int): Int = 
      if (a > b) 0 else f(a) + sumF(a + 1, b) 

    sumF 
  } 
   // 使用高阶函数重新定义求和函数
   def sumCube: Int = sum(x => x * x * x) 
   def sumSquare: Int = sum(x => x * x) 
   def sumFact: Int = sum(fact) 
   def sumInt: Int = sum(x => x) 

   // 这些函数使用起来还和原来一样 ! 
   sumCube(1, 10) 
   sumInt(1, 10) 
   sumSquare(1, 10) 
   sumFact(1, 10)

能不能再简单一点呢?既然 sum返回的是一个函数,我们应该可以直接使用这个函数,似乎没有必要再定义各种求和函数了。

清单 8. 直接调用高阶函数
   def fact(n: Int): Int = 
 if (n == 0) 1 else n * fact(n - 1) 

   // 高阶函数
   def sum(f: Int => Int): (Int, Int) => Int = { 
    def sumF(a: Int, b: Int): Int = 
      if (a > b) 0 else f(a) + sumF(a + 1, b) 

    sumF 
  } 
   // 这些函数没有必要了
   //def sumCube: Int = sum(x => x * x * x) 
   //def sumSquare: Int = sum(x => x * x) 
   //def sumFact: Int = sum(fact) 
   //def sumInt: Int = sum(x => x) 

   // 直接调用高阶函数 ! 
   sum(x => x * x * x) (1, 10) //=> sumCube(1, 10) 
   sum(x => x) (1, 10)           //=> sumInt(1, 10) 
   sum(x => x * x) (1, 10)      //=> sumSquare(1, 10) 
   sum(fact) (1, 10)             //=>  sumFact(1, 10)

我自己在Intellij里面也写了一下这一块的功能(我用的是first-spark-demo这个project):

package com.spark.my

import scala.annotation.tailrec

object Hello{
  def main(args: Array[String]): Unit = {
    val ret = sum(x=> x*x)(1, 2)
    println(ret)
  }

  def sum(f: Int => Int): (Int, Int)=> Int = {
    def sumF(a: Int, b: Int): Int =
      if (a > b) 0 else f(a) + sumF(a+1, b)

    sumF
  }

}

这种返回函数的高阶函数极为有用,因此 Scala 为其提供了语法糖,上面的 sum函数可以简写为:

清单 9. 高阶函数的语法糖

我在Intellij里面实际写了一下:

package com.spark.my

import scala.annotation.tailrec

object Hello{
  def main(args: Array[String]): Unit = {
    val ret = sum(x=> x*x)(1, 2)
    println(ret)
  }

  def sum(f: Int => Int)(a: Int, b: Int): Int =
      if (a > b) 0 else f(a) + sum(f)(a+1, b)

}

注意,以上语法糖和原来的写法,还是有很多区别的:

首先sum()()两个括号直接没有冒号

另外 ()(): Int 中间是冒号,不是=>

然后再下面的表达式中,用到了上面第二个括号里面定义的变量a和b

我们把原来的 sum函数转化成这样的形式,好处在哪里?答案是我们获得了更多的可能性,比如刚开始求和的上下限还没确定,我们可以在程序中把一个函数传给 sumsum(fact)完全是一个合法的表达式,待后续上下限确定下来时,再把另外两个参数传进来。

对于 sum 函数,我们还可以更进一步,把 a,b 参数再转化一下,这样 sum 函数就变成了这样一个函数:它每次只能接收一个参数,然后返回另一个接收一个参数的函数,调用后,又返回一个只接收一个参数的函数。

这就是传说中的柯里化,多么完美的形式!在现实世界中,的确有这样一门函数式编程语言,那就是 Haskell,在 Haskell 中,所有的函数都是柯里化的,即所有的函数只接收一个参数!

注:柯里化(currying):又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

清单 10. 柯里化

我在Intellij里面写的:

package com.spark.my

import scala.annotation.tailrec

object Hello{
  def main(args: Array[String]): Unit = {
    val ret = sum(x=> x*x)(1)(2)
    println(ret)
  }

  def sum(f: Int => Int)(a: Int)(b: Int): Int =
      if (a > b) 0 else f(a) + sum(f)(a+1)(b)
}

可以看到,非常巧妙!相当于接收第一个参数,返回接受后续参数的函数,直到参数个数足够为止,得出最后的结果。

结束语

在 Scala 类库中,使用函数式编程的例子比比皆是,特别是对于列表的操作,将高阶函数的优势展示得淋漓尽致,限于篇幅,不能在本文中为大家作以介绍,作者将在后面的系列文章中,以 Scala 中的列表为例,详细介绍高阶函数在实战中的应用。(还没找到后面的文章在哪里)

参考资料

学习

后续再看看JS里面关于函数编程的部分吧。关键是把函数编程的思想领悟了。

原文地址:https://www.cnblogs.com/charlesblc/p/6110484.html