缘起
前几天偶然看到我的To Do里有一条内容是关于学习Scala的。虽然记不起是为什么以及什么时候加进去的,但是出乎意料的让我突然很有试一试的冲动。
虽然对它早有耳闻,如果单纯只是看个大概,参考着Java的语法,也能猜个七七八八。但真正提起兴趣,大概率是之前准备看Spark源码的时候,然而当时应该是打开RDD的定义看了不到10行就放弃了。
这一次重新燃起兴趣,我觉得应该是出于以下几点:
- 用Java用的越久,越觉得自己的思想被禁锢了。(比如没有Class怎么面向对象呢)
- 人意识到自己被束缚就会想逃离,我想去FP的世界看一看。
- 从类C语言,直接切到类Lisp语言的跨度有点大,即使有Scheme打底,对于很多函数式的东西还是理解不了。(比如CPS和Monad)
- Scala是一门介于两者之间的多范式语言,我希望它可以带我更平滑的进入FP的世界。
- 因为比较平滑,我也希望借此可以把FP的理念安利给更多人。(特别是我的小师妹)
当然,其实今年已经粗粗的看过JS和Swift,所以没准Scala会成为今年“学会”的又一门语言 :-)
Scala的网评是杂和难学,不过作为一个有5年Java经验和1年Scheme经验的人,我觉得学习曲线应该不至于太陡峭。也趁着这次机会刚好完成一下之前的一个小梦想:通过和Java对比的方式学习一门新语言。
这个专题,我准备一周更新一次,每次按照这周对于Scala的理解用自己的话转述出来。可以帮助到有志于学习Scala的你。
第一周
这周是专题启动后的第一周,总结了一下之前失败的经验,应该是过于自信了,觉得都是JVM的语言,反正可以从字节码反编译成Java,就连语法都不想学(捂脸)。
这次还是好好规划了一下学习路径。知乎上有推荐的书单[1],本来还在纠结从哪本入门。后来想想,小孩子才做选择题,成年人当然是都看啦。
所以我随手挑了一本《Scala学习手册》先看起来。这本书不厚,200多页的样子,豆瓣评分9.0分,预计一个月左右能刷完。
(不过看到第5章这个翻译,首类函数,我盲猜一个应该是First Class吧,这个翻译质量也是有点堪忧。)
扯来扯去,终于进入正题了。接下来,我们来疾风式的讲讲Scala的基础语法。
变量和值
在Java里面如果我们要定义一个变量,一般的写法是:
Integer i = 1;
在Scala里写成:
var i : Int = 1;
这里把类型放在了identifier后面,曾经在学习Go和Swift的时候非常不习惯这种方式,不知道为什么突然感觉看着还挺顺眼的。
var这个关键字,说明i是可以被重新赋值的。如果要求i是常量,对应Java里面的final类型,那么需要用val来声明它。var是variable的缩写,val是value的缩写。
Scala虽然也是一门静态语言,但是变量的类型实际上并不需要显式的声明。也就是说,你写成var i = 1;
,编译器可以自动进行类型推导,得到i应该Int类型,可以说是既兼顾了静态语言的安全性,又兼顾了动态语言的简洁性。(对标下Swift其实也有这种能力,大概是现代语言的标配吧)
(类型推导一直是我想研究的内容,希望后续有时间专门开一篇叨叨一下)
字符串
接下来是关于三重引号。如果你试过用Java去拼接SQL的话,大概率被不能漂亮的换行所折磨。在Scala里,可以这么写:
val sql = """
select *
from table
where field = ...
"""
很优雅有木有?换成Java的写法,一堆加号一定还是扎到你的眼睛,不过u1s1,后续的Java版本里(忘了具体是11还是14)是有这个支持。但是大家依然停留在8不是吗~
另一个优雅的特性是关于字符串的内插(这个特性第一次见是在Perl里),比如说要给刚才的语句中的field1 = 带上具体的value,在Java里你要么用个加号,要么就只能用String.format了。
而在Scala里,可以这么写:
val fieldvalue = "ABC"
val sql = s"""
select *
from table
where field = $fieldValue
"""
(注意那个"s",带上它才可以实现内插哦~)
类型
Any是所有类型的父类,对应Java的Object。Nothing是所有类型的子类,Java里没有对应的类型。Null也比较奇怪。(这块等后续再来补吧)
元组
Java的函数只允许返回单个值,如果要同时返回code和message,多半需要定义一个Response类作为container了。
Scala提供了另一种可选的方案:直接返回一个2元组,定义2元组的形式如下:
// n元组通用写法
val res = (200, "成功")
// 或者2元组专用写法
val res = 200 -> "成功"
可以通过res._1和res._2等形式访问元组的元素。当然,元组并不限制元素的个数,不过从可读性上来说,如果元素个数大于3还是老老实实定义一个类吧。
表达式和语句
一句话说说表达式和语句的区别:表达式可以被求值,语句没有值。
典型的表达式,比如1 + 2
典型的语句,比如import和println
Scala有表达式块的说法,把多个表达式用大括号包在一起,最后一个表达式作为表达式块的值。这个说法应该是函数式的说法。在Java里虽然也有花括号,但没有相应的语义。
val area = {
val pi = 3.14
pi * 10
}
类似的写法在Java里会编译不通过,我想了想最接近的写法可能是Stream里面的map,你可以用一个花括号包裹很多表达式,但是最后还是需要一个显式的return。当然,如果只有一个表达式,不需要显式的return。
模式匹配
模式匹配粗看起来像Java里的switch。比如把code为200映射为成功,非200映射为失败。
val code = 200
val message = code match {
case 200 => "成功"
case _ => "失败"
}
最后那个case后面跟着的下划线,可以先理解成switch里的default。(实际上还是有一点不一样,后面详细展开[2])
有几个点不同的是,Java的switch没有返回值,模式匹配实际上会返回匹配上的那个模式的箭头之后的表达式或者表达式块的执行结果。
switch需要手动写break,而模式匹配每次只会匹配一个值,不需要手动break。如果你想一次匹配多个值,需要用管道符号把多个值合并在一起:
val day = "MON"
val kind = day match {
case "MON" | "TUE" | "WED" | "THU" | "FRI" => "工作日"
case "SAT" | "SUN" => "周末"
}
模式匹配里面还可以再套if,书里管这种写法叫模式哨卫,这个词乍一听不太好理解,如果你联想一下Java里的卫语句,就能明白它是什么意思了。
val msg : String = null
msg match {
case s if s != null => println(s"接收到 $s")
case s => println(s"无法处理")
}
注意这个例子里,case后面跟了一个变量s,其实是一种专门的用法,相当于把变量msg又赋值给了s,当然你也可以叫其他名字,赋值之后,可以用于if的判断,也可以用于匹配后的表达式块。
前面说的下划线,其实是变量绑定的特殊情况,实际上是把变量绑定给下划线了,以此来间接达到default的效果。
关于为什么是下划线,书里提到是因为在数学运算的时候经常用下划线代表未知数,比如:
5 * _ = 15
(可以作为一个小小的冷知识)
这个例子可能会让人觉得,我直接写if...else不好么?干嘛整这么费劲?暂时还不能很好的回答这个问题,不过书里又提到一个冷知识,也就是Scala里其实没有else if这种写法,在处理的时候,实际上是把后面的if作为上一个else之后跟着的表达式块来处理的。(真·符合奥拉姆剃刀原则,再补充一个冷知识,其实Lisp也是不需要else if的)
当然,模式匹配并不是switch的简单增强,它的核心应该是解构,Java在14里也引入了模式匹配,这里先挖一个坑,后面再慢慢填。