虽然Twiiter提供了中文版Scala教程:Scala 课堂,但是这只是一个参考书,并不适合Scala初学者,Scala与Java/C#不同特点是面向函数编程,这反映在语言上的不同点,具有Java或C#经验的程序员如果了解这个不同特点,也就是掌握了打开Scala语言大门的钥匙。
Scala的特点函数是第一等公民,而不是对象Object,函数可以作为方法参数传递,在Java中我们初学者经常要搞清一个问题,Java是按值还是按引用传递,现在Scala还可以按函数传统,也就是把函数看成一个对象引用。
准备Scala环境,在http://www.scala-lang.org/download/下载scala,启动命令行,可以输入Scala的语句了。
首先我们从Scala的函数开始:
在命令后输入:(x:Int) => x * 2
这种奇怪的格式让我们陌生,但是如果你熟悉javascript的函数如下:
function myfunc(param){
alert("hello" + param);
}
这是一个弹出窗口hello的函数,显示的是hellp+输入参数,这个param不只是可以传入值,也可以传入另外一个函数,为了能够传入另外一个函数作为参数,被传入的函数在写法上要改变一下,比如:
var myfunc2 = function (param){
alert("hello" + param);
}
可以看到,我们将函数名称移到了左边,右边就剩余function和()以及{}三个符号,这样我们可以传入myfunc了:
myfunc(myfunc2);
我们已经理解了JS中函数传递,那么Scala中也是类似,上面的(x:Int) => x * 2 其实可以看成JS的(x:Int) { x * 2 } ,我们使用大括号替代右箭头=>,两者意思差不多(少一个function付)。等同于js:
var myfunc3 = function (x) {
return x * 2 ;
}
scala的x:Int类似Java的Int x,Int是x的类型,js是动态语言,所以类型定义是不需要的。Scala的写法是:
var myfunc = (x:Int) => x * 2
我们自己简写成(x:Int) => x * 2也可以,和myfunc一样到处引用使用,没有名称而已,也就是匿名函数,可以作为另外一个函数的输入参数使用。如:
myfunc2( (x:Int) => x * 2);
myfunc这个函数自己也可以被直接使用:
myfunc(2)
结果是4;
那么myfunc(myfunc(2))是多少呢,注意,这里不是myfunc2,而是myfunc自己。
myfunc(2)
结果是8; 相当于调用了两次自己。
面向函数编程经常形象的比喻成类似集成电路的输入输出一样:
输入--->函数运算 -->输出
所以,这里(x:Int) => x * 2也有这三种结构:
输入x---->函数运算x * 2 ---->输出x*2的结果。
输出x*2结果和x*2运算实际是捆绑在一起,是一体的,所以,一般我们就不显式象js中声明return x*2。Scala的"=>"符号的 右边可以认为代表细节,代表函数体,代表ReturnValue is “右边”.
第二步
有了前面热身,我们对函数是第一等公民有个初步印象,下面再看看函数如何作为值传递的:
val myList = List(1,2,3,4,5)
for(x:Int <- myList) yield myfunc (x)
yield是专门用于for循环,将新的结果写入到结果序列中,这里将myfunc(x)结果返回一个新的List,结果是:List[Int] = List(2, 4, 6, 8, 10)
下面我们引入面向函数编程最常用的一个函数map:
myList.map((x: Int) => x * 2)
结果也是List[Int] = List(2, 4, 6, 8, 10); 也相当以将集合myList中每个元素经过(x: Int) => x * 2运算得到结果。
从输入输出这个角度理解这个函数map,map的输入是(x: Int) => x * 2的输出,而(x: Int) => x * 2的输入是什么呢?是x,那么x从哪里来的?猜测可能是来自myList的每个元素。
这里引入一个函数组合子(Functional Combinators)定义,组合子是一个没有自由free变量的函数。什么叫自由变量?没有孤立的变量,比如上面的变量是x不应该是一个孤立变量,其来自于myList的元素。
这里的map对列表myList中的每一个元素都应用了 x * 2函数,并返回一个新的列表List(2, 4, 6)。我们称这个操作是map 组合子,同理还有filter操作。
for(x <- c; if cond) yield {...}
等同于:
c.filter(x => cond).map(x => {...})
或
c.withFilter(x => cond).map(x => {...})
注意到这里for中多了一个if语句判断,也就是对列表集合中元素进行if判断,这相当于使用了filter函数,filter对传入函数计算结果为false的全部删除。返回一个布尔值的函数通常被称为谓词函数[或判定函数]。
再看看for的另外一种形式, 集合嵌套:
for(x <- c1; y <- c2; z <-c3) {...}
等同于:
c1.foreach(x => c2.foreach(y => c3.foreach(z => {...})))
使用组合子foreach的好处这里也可以看出,使用for进行嵌套循环,经常可能会迷糊钻晕了,而使用组合子则简单明了。foreach 这个组合子类似Map,但是不返回任何结果,
val doubled = myList.foreach((x: Int) => x * 2) doubled: Unit = ()
这里foreach返回结果为类型Unit,类似void,是空。
flatMap 是另外一个组合子,flat可以理解为折叠或嵌套的意思。
for(x <- c1; y <- c2; z <- c3) yield {...}
等同于
c1.flatMap(x => c2.flatMap(y => c3.map(z => {...})))
这里的for循环和上面区别多了一个yield,看到yield第一反应我们是想到map,但是这里集合不是一个,而是三个嵌套,那么我们就使用flat+map.注意到,z是嵌套集合最后一个输出,作为map的输入。
再看一个例子,假设有嵌套集合如下:
val nestedNumbers = List(List(1, 2), List(3, 4))
nestedNumbers.flatMap(x => x.map(_ * 2)
返回结果是
List[Int] = List(2, 4, 6, 8)
将两个集合折合成一个集合输出,并应用了x*2函数计算。这里_ * 2等同于(x: Int) => x * 2,下划线_表示通配上下文的任何变量或函数。是一种简单写法。
第三步
让我们还是围绕(x: Int) => x * 2继续展开,它代表一个有输入和输出的函数,如果我们在另外一个函数中需要用这个函数作为输入参数,那么如何定义另外一个函数的方法参数呢?myfunc2(_*2)是一种hard code写法。
def myfunc2(fn: Int => Int): Int = {
fn(10)
}
这里的fn: Int => Int匹配(x: Int) => x * 2这样的抽象,当然也可以是(x: Int) => x + 2等,只要输入和输出返回都是整数即可。如果我们运行:
myfunc2((x: Int) => x * 2)
结果是20, 而运行:
myfunc2((x: Int) => x + 2)
结果是12。
在这里,fn(10)中的10是fn输入参数,fn输出结果是根据myfunc2的输入决定的,有点类似访问者模式哦。
下面我们尝试写自己的函数组合子:
var myfunc = (x: Int) => x * 2
val numbers = List(1, 2, 3, 4)
def ourMap(numbers: List[Int], fn: Int => Int): List[Int] = {
numbers.map((x: Int) => x * 2)
}
ourMap(numbers, myfunc(_))
结果是List[Int] = List(2, 4, 6, 8)