写给小师妹的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界。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容