每天两页,分享《Programming in Scala》的心得。
这个系列之前其实写过。具体可以看:
写给小师妹的Scala学习笔记·开篇
写给小师妹的Scala学习笔记·二
当时是为了快速“学会”Scala,选择了疾风式的搞法,几周就撸完一本书。
这次我们慢下来,找一本Scala作者自己写的书,仔细品一品Scala的设计哲学,并且以日记的形式记录一下所思所想。
以下是正文部分。
2022-01-02
今天读的这一章的大标题叫做“ A Scalable Language”。Scala这个名字,源于作者希望创造一种Scala-ble的语言。
接下来,他简要的介绍了Scala的几个特性:
首先,它是一种运行在JVM上的语言,因此和Java具有“无缝”的交操作性。借助于Java强大的生态,可以少造无数轮子。当然像Groovy、Clojure等等都有这样的特性。
其次,Scala兼有面向对象和函数式2种编程范式(这一点其实是相当不常见的),而且是一门静态类型的语言(所谓动态一时爽,重构火葬场,静态语言,可以让编译器替你干不少脏活累活)。函数式的一面使得它易于构建小组件,面向对象使得它可以用来构建大型程序。
接下来作者强调,编写Scala代码的过程是“fun”的。并通过一个Map的例子,作者展示了,Scala具备类型推导能力和易用的API。
var captial = Map("US" -> "Washington", "France" -> "Paris")
captial += ("Japan" -> "Tokyo")
println(captial("France"))
比如,不需要多余的分号,不需要声明Map
对比一下原生Java的写法:
Map captial = new HashMap<>();
captial.put("US", "Washington");
captial.put("France", "Paris");
captial.put("Japan", "Tokyo");
System.out.println(captial.get("France"));
2022-01-03
今天读的这一节的小标题是“A language that grows on you”,翻译过来应该是“一门会让你慢慢爱上的语言”。
接下来作者用两个例子,印证了这句话。这两个例子分别是,可自定义的数据类型(以BigInt为例)和可自定义的控制结构(以Akka的API为例)。在一番code show之后,作者都要表达一下“这些并不是语言内建(build-in)的特性,但是用起来和内建的没有区别”。
过程中,还引用了Eric Raymond《大教堂与集市》的说法,表达了Scala的设计哲学更接近于集市。
到这里,作者想表达的意思是比较明确的:Scala是一门内核极其精炼的语言,但却有着非常高的可拓展性。就好像集市一样,它并没有(也不可能)事先规划好所有的内容,但却拥有极强的演化能力。
2022-01-04
今天读的这一小节的标题是:“What makes Scala scalable?”。对于之前提到的面向对象及函数式编程再次做了个补充,并且可以说是干货满满。
首先作者讲了,Scala是OO的。而OO的本质是把数据和操作封装在一个容器中,这个容器就叫做Object。这样,操作变成了一种数据,容器本身也作为数据可以被传来传去。
同时,作者举了例子,说有些语言不是那么“纯”的OO,比如在Java里面,原始类型就不是对象(数组也不是),同时,Java还允许在class中定义静态的字段和方法。而在Scala中,任何的值本质上都是对象,甚至与1 + 2也是,它的底层是针对1这个Int调用了+这个方法,同时传入了参数2。
作者举的第二个例子是关于trait。它有点像Java中的接口,但是拥有自己的字段和方法实现。关于trait其实一直有一些困惑,希望后面的章节可以解答。
接着作者又讲到Scala是函数式的。这一段堪称“教科书”式的介绍。
作者介绍了,函数式编程的2个特点。函数是一等公民、函数调用需要做到引用透明,也就是没有副作用。
一等公民是指,函数可以在任意地方被定义(比如在一个函数里),还可以像数值一样作为入参或者返回(高阶函数)。
那什么叫有副作用的函数调用。包括:
打印日志、修改了函数的入参、从函数参数外(全局变量、bean、threadlocal)获取数据、抛出异常等等。对应的,一个没有副作用的,引用透明的函数,只有输入/输出,并且输出可以被等价替换掉。
比如1+sum(1, 1)中,sum函数如果可以直接用2替换,就说明是引用透明的。
2022-01-05
今天的这一小节,大标题是“Why Scala?”,作者从兼容性、简洁程度、高级抽象(?)和静态类型4个方面讲了,为什么要选择Scala。
第一部分是兼容性,除了前面提到的互操作性之外,还强调了,像底层的String、Int之类的都是复用的Java原生的类型。同时,Scala还通过一个叫“隐式转换”的概念,在不修改这些类源码的情况下,对这些类做了增强。
第二部分是简洁性。这个不多说了,差不多是同样功能的Java的代码量的1/4吧。后面又介绍了“trait”这个留到后面展开。
2022-01-08
中间的记录断了两天,不过问题不大。书还是在看的。
这一段,作者提到Scala是“high-level”的,实际上,作者想强调的依然是“函数式”编程范式带来的好处。
(正本清源)
函数式编程和面向对象编程并不冲突,用Java照样可以写出非常函数式的代码。
真正不太兼容的其实是描述式的风格和命令式的风格。
比如给定一个Int列表,求各元素之和。如果用命令式风格来写,起手一个sum = 0,后接一个i = 0,最后for循环收尾。
如果用描述式风格来写,“一个列表的各个元素之和” 等于 “列表的第一个元素” 加上 “列表其余元素构成的列表的各个元素之和”。
假设取列表的第一个元素用head表示,取列表的剩余元素构成的列表用tail表示。则伪代码如下:
sum(alist) = {
if(alist != empty) return 0
else return head(alist) + sum(tail(alist))
}
可以看到,描述式风格和递归是比较自然的一对组合,无怪于FP系的语言都喜欢用递归。
遵循函数式风格的另一个好处,是可以写出“引用透明”的函数。也就是说,这个函数调用的结果,可以用该函数的返回值等价的替换掉。
比如代码里有一段是:a + sum(b, c),如果b和c分别等于2和3的话,那么和直接写a + 5是等价的。或许因为这个例子太简单了,并且是数值计算,所以大部分人可以天然的写出这种引用透明的函数。
但,如果是一段业务代码,你能保证不在函数里调用Spring的bean去读写数据库吗?能保证不修改某个全局变量或者ThreadLocal吗?能保证不去调用某个入参的set方法吗?
引用透明的另一种说法是“没有副作用”,以上列的一些例子都是副作用的体现。
没有副作用的代码,易于测试和重构,也更少的引入bug。想想是不是经常发现某个字段,在经历了一系列的函数调用之后,不知道什么时候就被设置了一个不太符合预期的值,然后引出一系列莫名其妙的问题?
上面提到的最后一个例子,有一个专门的名字,叫aliasing problem,那么函数式的编程风格,是如何避免这样的问题呢?答案就是使用不可变的数据(immutable data)。
通过把一个类所有的字段都设置成final的,这个类就是一个不可变的类(如果有个字段是final的Map,非要去修改这个Map,就属于硬杠了)。
相应的,函数体里面也不再能set各种字段。一旦要改变些什么,要应该通过返回一个新的类的实例完成。
实际上对于大多数程序员来说,疑问都是,不可变的数据,能编程吗?看看Java的String,看看Spark的RDD。