写给小师妹的Scala学习笔记·二

第二周

这一周的内容主要围绕着“函数”来进行。本来想顺着书的内容往下讲,不过那样就没有自己的东西了,所以,就想到哪里写到哪里吧~

普通函数

我们从一个最普通的函数开始。它的作用跟函数名一样,你给它输入什么,它给你返回什么。

def echo(x : Int) : Int = {
    x
}

在语法上:

  1. 函数定义以def开头。
  2. 函数的返回值类型在函数名之后。
  3. 注意那个等号。
  4. 不需要显式的写return。(请问是为什么呢)

泛型函数

当然,这个例子只是为了引出接下来的例子,因为普通的版本,只能接受Int作为输入和输入,如果要支持任意类型,就需要让函数支持泛型。

def echo[T](x : T) : T = {
    x
}

通过在函数名之后的中括号和T,可以支持单个参数的泛型,如果要支持多个,可以写成类似[T, V]的形式。

带默认值的函数

好了,水完跟Java类似的部分,下面讲讲Scala做得好的部分。

相信你一定见过这样的代码,把一个字符串类型的日期,转换为Date,通常会根据不同的合适进行适配,比如20201226和2020-12-26。通常大部分日期格式都是第二种,因此会定义2个函数,一个需要str和pattern 2个参数,一个只需要str一个参数。

但是,在Scala中,你可以通过一个带默认值的函数来完成Java里2个函数的功能。

def str2Date(str : String, pattern : String = "yyyy-MM-dd") : Date = {
    …
} 

str2Date("20201226", "yyyy-MM-dd")

str2Date("2020-12-26")


在语法上,通过pattern参数后面加一个等号来完成。

递归和尾递归

递归大家都懂,但很多人都不知道尾递归是什么。

在我的理解里,尾递归就是满足这样条件的递归:递归函数的调用出现在函数的最后一行,并且,这一行有且只有这个函数调用本身。

比如下面的写法就是一个普通的递归函数。

def fact(i : Int) : Int = {
    if (i == 1) 1
    else i * fact(i - 1)
}

如果要改成尾递归形式,可以这么写。

def fact(i : Int) : Int = {
    @annotation.tailrec
    def inner(n : Int, result : Int) : Int = { // 1
        if (n == 1) result                     // 2
        else inner(n - 1, n * result)          // 3
    }                                          // 4
    inner(i, 1)                                // 5
}

这里用了一个嵌套函数的例子,证明了在Scala中函数属于一等公民。(这个点我们稍后再展开)

一次函数调用意味着JVM中一个栈帧的入栈和出栈,因此普通的递归函数容易出现“爆栈”。而编译器在看到尾递归的写法的时候,可以进行优化:把递归改为迭代,从而消除函数调用。

从递归到迭代是有范式的,比如上面的写法可以转化为以下的等价写法,不过,这是我的一个理解,而真正的实现,感觉可以再留一个坑在这里后面来填。

def fact(i : Int) : Int = {
    var result : Int = 1                            // 根据5的第二个参数
    var n : Int = i                                 // 根据5的第一个参数
    while (n > 1) {                                 // 根据2和3
      result *= n                                   // 根据3的n * result
      n -= 1                                        // 根据3的n - 1
    }
    result
}

(顺带提一句,在Java里是没有尾递归优化的说法的)

下面终于来到了FP(函数式编程)的世界(激动)

一等公民

先叨叨一下什么是刚才说到的一等公民。在Java的世界里,类是一等公民,因为你可以把它赋值给一个变量、你可以把它作为一个方法的输入和输出、你可以在任何地方定义一个类(在方法里、在类里、甚至是定义一个匿名内部类)。在Java的世界里,函数是“从属”于类的,这就会显得很繁琐和臃肿,比如要根据不同的方式给一个List排序,还需要定义一个排序的接口。

而在Scala的世界(或者说函数式的世界),函数就是一等公民,我们参考一下刚才的定义,你可以把它赋值给一个变量、你可以把它作为一个方法的输入和输出、你可以在任何地方定义一个函数(比如刚才那个例子里面的inner就是定义在fact里面的辅助函数)。

当我们用i这个变量,val i : Int = 1,去接收一个值的时候,实际上我们需要声明i的类型是什么。而现在,要支持把函数赋值给i这样的特性,对于程序员来说是有“额外”的负担的。那就是你需要知道函数的类型,并把它显式的声明出来。

在写Java的时候,其实你不会那么的关注一个方法的类型。比如刚才的fact函数,类型是(Int) => Int,而inner函数的类型是(Int, Int) => Int。可以看到,函数的类型和入参的类型及个数,以及返回值都相关。(这和Java里判断方法是否重载的逻辑不同)。

(考考你,Int => Int => Int,代表的是怎样的一个函数?)

高阶函数

刚才讲了函数赋值给变量i的场景,那相应的,一个函数A可以作为另一个函数B的入参或者返回值。像B这样的函数,我们称为高阶函数。

大家最耳熟能详的高阶函数应该是Map和Reduce。如果是从Java或者Spark切换过来的,可能对于Map的理解是Stream的一个方法或者RDD的一个方法。实际上最正统的Map(来自于Lisp之父约翰·麦卡锡),应该支持2个参数,一个参数是一个集合,另一个参数是一个函数。它的作用是把函数应用在每一个集合元素上,并返回一个新的集合。


// 定义一个map函数
def map(f : Int => Int, c : List[Int]) : List[Int] = {
  var res = List[Int]()
  for(e <- c) {
    res = res :+ f(e)
  }
  res
}

// 将列表的每个元素值翻倍
map(e => e * 2, List(1, 2, 3, 4))

// 将列表的每个元素值加1
map(e => e + 1, List(1, 2, 3, 4))

基于高阶函数,可以实现Java中很多的设计模式,比如在高阶函数中定义好一个通用的骨架,然后把需要动态变化的部分,通过函数参数传进来,能实现类似于模版模式、策略模式、代理模式等多种效果。

多扯几句,高阶函数这个词,听着比较学究,实际上,如果把上面那个例子的=>改成->就是Java中的Lambda表达式的写法了,从使用的角度来说,我们不需要高阶函数这样的说法,不过如果在面试时候能把这一套讲清楚,无疑是一个加分项。

更多关于Java中的Lambda的内容,可以参考《State of the Lambda》[2]这篇文档(需要的化,可以翻译给你哦)。

柯里化

上面讲了以函数作为参数的高阶函数,下面我们讲讲以函数作为返回值的高阶函数。

假设有这样一个东西(A):

当我输入1时,它会返回给我这样一个东西(B),接受一个Int型的输入,并返回输入加1后的结果。

当我输入10时,它会返回给我这样一个东西(C),接受一个Int型的输入,并返回输入加10后的结果。

在Java的世界里,我们可能会定义一个叫Increase接口,B和C都是它的实现类。,而A则是Increase的工厂。

一个字,繁琐。在FP的世界里,我们Duck不必这样。

def increaseMaker(i : Int)(n : Int) : Int = {
    i + n
}

increaseMaker(5)(10)
// 得到15

val increase1 = increaseMaker(1) _
val increase5 = increaseMaker(5) _

increase1(10)
// 得到11

increase5(10)
// 得到15

定义一个构造increase的函数,它有2个参数。

通过increaseMaker(1) _我们绑定了它的第一个参数,在第二个参数的位置,用占位符代替,代表未来再来绑定。这样我们就构造了一个部分应用的函数。

再通过类似increase1(10)的形式,拿到最后的结果。

这底层所用到的技术,我们称之为柯里化。它是指,一个多参数的函数,可以接受一个参数,并返回一个可以接受其余参数的函数的技术。

而返回的那个函数,我们成为闭包。意思是,一个携带着状态的函数,而这个状态或者说变量的定义,来自于函数之外。

刚才那个例子,实际上还可以这么写,我认为参数组,就是下面这种写法的语法糖:

def increaseMaker(i : Int) : Int => Int = {
    def inner(n : Int) = {
        i + n
    }
    inner
}

因为它,像Lambda演算这样只能支持单参数的东西,才有了支持多参数的能力。

插播一下,柯里化这个词,是为了纪念数学家柯里而命名的。他的全名叫Haskell Curry,first name和last name都贡献给了PL界。

你可能感兴趣的:(写给小师妹的Scala学习笔记·二)