最近又在学习scala(好吧,为什么我要说又呢?)。为什么又要学习scala呢?因为有一天我看到一段java代码的时候,不知道为什么,有点想吐了。于是又开始学习scala。然后基本是通过读《programming in scala》同时配合一些网上的资料来学习。接下来就随便聊聊scala的学习吧。
抽象
什么是抽象呢?据说就是从众多的事物中抽取出共同的、本质的特征,而舍弃其非本质的特征。我理解的是:将多个事物的共同的地方抽取出来,然后给它一个名字。所以有几点:
- 事物的共同特性。
- 将事物的属性进行抽取,也就是说聚焦在某个或者某几个属性上,同时忽略其他属性
- 给它一个名字
举个例子,比如“前肢”这个抽象,就是将动物的距离头部较近的肢体抽取出来,忽略掉其他的部位,然后给一个名字。
在进行抽象的时候,往往需要知道事物的属性有哪些,然后聚焦在一个或者几个属性上,看看是不是应该进行抽取,命名。
分类
分类,又可以叫分门别类。就是把具有相似属性的东西放在一起,形成一个门类。分类是人类进行组织的一个重要方式。典型的例子就是图书馆,分类存放,同时分类检索,以此提高使用效率,减少检索时间。
还原论
先来看一下百度百科的定义:
最新的大不列颠百科全书把还原论定义为:“在哲学上,还原论是一种观念,它认为某一给定实体是由更为简单的或更为基础的实体所构成的集合或组合;或认为这些实体的表述可依据更为基础的实体的表述来定义。”还原论方法是经典科学方法的内核,将高层的、复杂的对象分解为较底层的、简单的对象来处理;世界的本质在于简单性。
还原论,是西方哲学中的一种思想,或者说思维方式。即认为复杂的事物可以由初级或者基本的事物组成。比如组成物质的基本是分子,分子又有原子。强调将复杂事物分解成基本事物,将基本事物组成复杂事物。
编程语言
语言,我认为就是一堆符号,用于记录和交流。比如你想跟美国人交流,可能你就得会英语。
编程语言,就是跟计算机交流的一堆符号。那么,问题来了,我们为什么要跟计算机交流呢?交流的目的是什么?人和人之间的交流目的可能有很多,有情感上的,经济上的等。但是跟计算机交流,我觉得目的就比较单纯了。计算机能做什么事情呢?计算。所以跟计算机的交流主要是希望计算机能帮忙完成一些计算工作。
计算
查了一下百度,数学上,计算是指“一种将单一或复数之输入转换为单一或复数之结果的一种思考过程”。没什么感觉也没关系。来看看具体的计算,比如在数学上的计算就是数及其加减乘除,而对于字符上的运算,就有诸如拼接,加减之类的了。这个时候,我们可以直接结合语言来看计算的具体形式。
一般来说,计算都有两个要素:
- 计算方式,比如加减乘除
- 计算所使用的数据,比如整数,浮点数
用另一个名字来说,我们可以理解为数据及其操作。比如1 + 1 = 2,两个1就是数据,加就是数据上的操作。
有数据,就会对数据进行分类,也就有了数据类型。为什么要对数据进行分类呢?一是能够更好的理解;二是能够更好的管理。分类之后,针对每种类型,又有具体的操作。那么我们首先来看看scala中对数据的分类。
第一个是将数据分为基本类型和复合类型。区别在于复合类型是由基本类型组成。换句话说复合类型就是将多个基本类型放在一起。
这里先看看基本类型,复合类型后面会谈到。基本类型有:
- 数字(numeric types)。一般来说有整型和浮点型,整型表示整数,浮点型表示带有小数的数字。整型一般有Byte, Short, Int, Long, 其区别是取值范围,或者说是位数。浮点型往往有Float, Double型,区别也是取值范围。
- 字符。这里包括了字符和字符串。字符串就是一系列的字符。
- bool。一般来说包括了true和false。
这里有一个成为”literal"的东西,中文一般翻译为”字面量“。来看看是什么意思:
Taking words in their usual or most basic sense without metaphor or exaggeration.
就是说用最常用或者最基本的方式来表达一个意思,其中没有任何隐喻或者夸张的成分。就是说直接简单。比如,你想有一个字符串,直接写“hello world“,这个东西就是一个literal。同样,直接写4,4.5,true,false,也是literal。
有了基本数据类型,就会有针对每种数据类型的计算。这里就有一个数据及对数据的操作的意思了。对不同的数据可能的操作有:
- 数字类型。这个地方跟数学很想,基本的就有加、减、乘、除、取余等。同时也可能有大于,等于,小于等比较操作。
- 字符类型。最先想到的就是拼接,从一个字符串中找到其中一个字符等。
- bool类型。与、或、非是基本的操作。
有些操作是各个类型都可能有,比如等于,不等于。还有一些额外的操作,比如bitwise,就是位运算,比如|, &, ~, ^, >>, <<等。
对于数据,最直接的体现就是literal。对于操作,最直接的体现就是操作符(Operator)。比如+, -, *, /, %等。
操作符根据位置不同又分为三种,分别为:中置(infix),前置(prefix),后置(postfix)。被操作的数据称为操作数。中置就是说操作符在操作数的中间,这也说得有两个操作数,前置和后置就是操作符一个在操作数的前面,另一个在后面。举一些列子:
- 中置,7 + 4,这里的+就是中置操作符
- 前置,-2。scala里面的前置操作符只有+, -, !, ~。
- 后置,好像还真没有什么操作符是后置的。不过在scala里面,操作符实际上是方法,就是说7 + 4实际上是在7上调用了+这个方法,然后传入了4这个操作数。又可以写成(7).+(4)。所以,没有参数的方法调用就算是后置操作符了。比如"Hello World" toLowerCase。这里实际上是"Hello World".toLowerCase。只是scala里面这种调用情况下可以省略那个点。至于什么时候可以省略,后面会提到。
基本到这里,就基本介绍完计算了。就是说,此时应该可以通过scala这门编程语言让计算机完成一些计算了。
多行
有了数据及操作之后,我们就可以在scala的REPL上来进行一些简单的计算了。在mac上的terminal直接输入scala,然后完成一些计算:
在REPL里面有个问题就是,如果是简单的计算,比如7+4这种就很方便。但是如果是比较复杂的计算呢?比如计算一个阶乘。这个时候简单的单行程序就不行了。解决问题需要多行代码。
牵涉到多行,就有一个分隔符的问题。java里面每行的结果必须要有一个分号。这个分号就类似于一个分隔符。幸运的是scala里面并不需要在行末使用分号。
除了多行之外,完成比单行稍微复杂的计算,就需要将一些小的计算结果暂存起来,然后在小的计算结果上进行计算。这时候就需要变量。变量就是一个可以改变的符号。对应的还有不变量,就是常量。具体的声明,在scala里面,变量使用var(variable),常量使用val(value)。比如:
var x = "I am a variable"
val y = "I cannot change"
在有了常量或变量,多行之后,就可以实现比单行复杂的代码了,比如计算10的阶乘:
val firstPart = 1 * 2 * 3 * 4 * 5
val secondPart = 6 * 7 * 8 * 9 * 10
val result = firstPart * secondPart
当然,这完全可以放在一行。不过作为举例,不必太过苛责,意达即可。
在定义变量或者常量的时候,也可以显示定义其类型,比如:
val firstPart: Int = 1 * 2 * 3 * 4 * 5
val secondPart: Int = 6 * 7 * 8 * 9 * 10
val result: Int = firstPart * secondPart
这时候有一个问题,如何运行上面的代码呢?
一种方式是将上述代码写在一个以scala作为后缀的文件里面,比如叫"multi_line.scala"。然后在REPL里面load该文件。比如:
另一种方式就是使用scala script:
#!/usr/bin/env scala
val firstPart = 1 * 2 * 3 * 4 * 5
val secondPart = 6 * 7 * 8 * 9 * 10
val result = firstPart * secondPart
println(result)
上面的内容放在了一个叫”multi_line_script.sh"的文件中,怎么运行呢?
- 首先
chmod a+x multi_line_script.sh
- 然后使用
./multi_line_script.sh
执行
我在mac下执行成功。
控制结构
有了多行之后,这时候实际上就有控制结构了。想到控制结构,就想到典型的三种:顺序,分支,循环。上面的例子就是顺序结构,即先执行什么后执行什么。循环结构并不是必须的,不过有还是ok的啦。
分支结构:
val x = 10
val result = if(x > 10) "larger than 10" else "smaller than or equal to 10"
已然是使用了"if"和"else"的组合。注意到if的结果赋给了常量result,这里表明if这个表达式是有结果的。
if和else只是两个分支,多个分支的情况怎么办呢?一种方法是在else里面继续if-else。还有就比如java里面的switch-case。scala里面有一种叫"pattern match"的东西。先来看个例子:
val animal = "dog"
val resultAnimal = animal match {
case "cat" => "c"
case "dog" => "d"
case _ => "a"
}
resultAnimal
的结果是d
。match
前面的东西叫做"selector",后面跟一个大括号,里面有多条case
语句,一个case代表一个分支。最后的case _ => "a"
,其中的_
表示匹配所有情况(类似于一个default分支)。_
表示不在乎具体的内容是什么。
循环:
循环一般有两种,一种是while,另一种是for。先看while:
var i = 0
while (i < 10) {
println(i)
i += 1
}
好像并不需要解释什么。注意这里要使用var。
还有就是for循环了:
for(j <- Array(1,2,3,4,5)){
println(j)
}
需要注意的是,j这个量并没有显示定义,然后j并不是一个变量,而是一个常量。Array表示一个数组。上面的for循环就是遍历了这个数组,然后将值打印了出来。符号"<-",可以认为是数学里面的属于符号。
scala的for循环还有其他一些东西,以后再谈了。具体内容可以参考《Programming in Scala》。
函数
有了数据,操作,多行,控制结构。这时候我们就可以写稍微复杂一些的程序了。比如阶乘的计算:
var result = 1
for(j <- Array(1,2,3,4,5)) {
result *= j
}
这里有两个问题。
- 第一,上面这个只能计算5的阶乘,如果要计算10的阶乘,就得再写一遍,只不过把数组那部分给替换掉。这就是重复。
- 第二,从阅读的角度来看,我并不能一眼看出这段代码是干什么的,必须一行一行一字一字去读,然后才能明白。“哦,原来这是在算5的阶乘啊”。没有办法去忽略细节,只关注我们想要关注的地方。
为了解决上面两个问题,我想到了函数。函数是什么?我认为是一种抽象和封装,它将一段代码包(wrap)了起来,然后给了一个名字。同时还有对应的输入和输出。
所以,函数的三要素:输入,输出,函数名。当然了,肯定还要函数体(空的函数体也算啦)。
来看看scala里面对于函数的定义:
def max(x: Int, y: Int): Int = {
if (x > y)
x
else
y
}
几点:
- 使用def关键字表明定义的是一个函数
- 函数名的位置就直接在def后面
- 函数名后面跟着参数列表,放括号里面
- 每个参数有自己的类型
- 函数的返回值类型。跟在括号的后面
- 等号,表明后面的就是函数体了
- 使用大括号来限定函数体。就是说函数体就在大括号里面啦。
这里面有def,函数名这些。其实这些都不是特别必要的,我们可以看看原始的没有任何夸张和想象的形式,function literal。来看一段代码:
Array(1,2,3,4,5).foreach(arg => println(arg))
foreach
是Array上的一个函数,这个函数可以接受另一个函数作为参数。arg => println(arg)
就是一个function literal,有时候也就叫匿名函数(因为没有名字嘛)。函数三要素。一,没有函数名;二,输入,就是=>
前面那个;三,输出,没有显示声明会输出什么。四,函数体,=>
后面的就是。
function literal还有一些别的写法,比如:
val withType = (x: Int) => x + 2
val doesNotCare = (_: Int) => 10
val multiLine = (x: Int) => {
val temp = x + 10
x + temp
}
withType(2)
doesNotCare(3)
multiLine(10)
需要注意的是val doesNotWork = x => println x
是过不了编译的。在REPL里面报x没有定义类型。但是在Array.foreach
里面是可以使用的,因为Array.foreach
定义了其参数的参数类型(其参数是一个函数)。
说到function literal,不由得联想起closure来。那么什么是closure呢?
先来看看《Programming in Scala》里面的一段话:
The name arises from the act of "closing" the function literal by "capturing" the bindings of its free variables.
"the name"指的就是closure。看了这个就有几个问题,什么叫closing?什么是function literal?什么叫free variable?什么是capturing?
什么是function literal这个前面已经解释过了。就是一种相对简单基本的函数定义形式。
什么叫closing呢?
close就是关闭,所以我认为,这里的意思就是函数定义完成了。"closing the function literal"就是说函数结束了。函数体已经写完了。
什么叫free variable?举个例子:
(x: Int) => x + more
这里x是参数,more并不是。其并未定义。这里的more就叫free variable。就是说在当前function literal里面没有被定义的变量。而对应的x就叫做bound variable。
来一个能编译通过的例子:
var more = 10
val addMore = (x: Int) => x + more
这里的addMore就是一个closure。相对的,val addOne = (x: Int) => x + 1
就不是一个closure。因为里面并没有capture一个free variable。所以,所谓capture就是指funtion literal里面使用了一个外部的变量。
需要强调的是,closure是跟funtion literal紧密联系起来的。感觉上相当于function literal的一个特例。
在scala里面,closure可以改变free variable的值,同时外部对free variable的改变也会反映到closure里面。
提到函数,就得提下函数式编程。人们说,函数式编程有一些特点:
- 函数也是值。可以用来赋值,可以用来给函数传递参数。所谓的“一等公民”。
- 没有变量,全都是常量。
- 聚焦于函数,函数是基本的思考单元。
当然,函数式编程的范式不止这些。不过我觉得上面三点算是比较基本和重要的。
还有,函数实际上也是一种抽象,它隐藏了细节。函数也是一种形式的组件,可以用来组和成更大的组件。
一个稍微复杂点的例子
需求很简单,就是给一个字符串数组,将这个数组放在一个矩形框中打印出来。比如:
输入:Array("what", "a", "good", "day")
输出:
一个可能的实现:
val symbol = "*"
def repeat(s: String, times: Int): String = {
var result = ""
(1 to times).foreach(_ => result += s)
result
}
def preSymbol(s: String): String = symbol + s
def postSymbol(s: String, l: Int): String = s + repeat(" ", l - s.length) + symbol
def generateFrame(array: Array[String]): String = {
val largestLength = array.maxBy(_.length).length
array.transform(x => postSymbol(preSymbol(x), largestLength + 1)).mkString("\n")
val aSymbolLine = repeat(symbol, largestLength + 2)
aSymbolLine + "\n" + array.mkString("\n") + "\n" + aSymbolLine
}
println(generateFrame(Array("what", "a", "good", "day")) + "\n")
println(generateFrame(Array("w", "a", "g", "d")) + "\n")
println(generateFrame(Array("", "a", "g", "d")) + "\n")
println(generateFrame(Array("w", "a", "g", "")) + "\n")
println(generateFrame(Array("w", "a", "", "d")) + "\n")
println(generateFrame(Array("what", "a", "", "d")) + "\n")
println(generateFrame(Array("what", "a", "gooooooooooooooooooooooood", "day")) + "\n")
这篇就先到这个地方。总结一下,先介绍了抽象、分类、还原论、编程语言等。然后从计算说起,聊到数据及其操作,具体的就是数据的类型,基本数据及复合数据,然后不同类型对应的操作。然后是多行,多行就牵涉了行分隔符,变量,常量,scala script。控制结构,就想到了顺序、分支、循环。其中分支包括了if-else和pattern matching。然后是函数,想到了匿名函数和closure,以及函数式编程。
最后,本文并不追求scala的各种语言细节,只是想找一根线将scala中的各个大的概念串起来。