函式式語言的體驗
序言
這一次講的不是作為Java改良版的Scala語言中所具有強大的純面向物件功能,而是以函式式語言來介紹他。函式本身是物件,他可以被賦值給變數,或者作為方法的引數來傳遞,我們把他作為“第一類物件”來看一下他的處理方法。另外也讓讀者體驗一下函式式語言特有的模式匹配的強大功能。好,讓我們馬上出發,開始我們第三次迷你旅行吧。
Scala的函式定義
在Scala中方法被作為函式用def語句以“def 函式名(引數類表): 返回值 = 函式體”格式來定義。
- def foo(s: String, n: Int): Int = {
- s.length * n
- }
但是函式體僅由單個句子來構成的話可以省略{}。
- def foo(s: String, n: Int): Int = s.length * n
還有,型別推斷對於返回值也是有效的,在允許的情況下是可以省略他的型別的(函式定義中,引數的型別則不可省略)。但是為了理解方便,除了互動式環境下以指令碼語言方式使用外,還是作為標記保留下來比較好吧。
- scala> def foo(s: String, n: Int) = s.length * n
- foo: (String,Int)Int
- scala> foo("Zhang Fei", 3)
- res0: Int = 27
為了宣告無返回值的函式可以將返回值定義為Unit。這個與Java中的void相同。
- def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s)
上述函式的目的是為了執行被認為是副作用的列印n次傳入字串,所以返回值是Unit。附帶說一下,Unit唯一的例項是用()文字來表示。
引入單例物件內的方法
這些方法一般都定義在類之中,但是如果想單獨使用它的話,通常將其定義在單例物件中。
- object MyFunctions {
- def foo(s: String, n: Int): Int = s.length * n
- def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s)
- }
為了使用foo或bar這些方法,通常指定單例物件名和方法名來呼叫他。
- scala> MyFunctions.foo("Zhang Fei", 3)
- res1: Int = 27
- scala> MyFunctions.bar("Zhang Fei", 3)
- Zhang FeiZhang FeiZhang Fei
如下所示將方法引入之後就不用一次一次的指定單例物件名了。下面引入了所有MyFunctions裡的方法。
- scala> import MyFunctions._
- import MyFunctions._
- scala> foo("Zhang Fei", 3)
- res0: Int = 27
- scala> bar("Zhang Fei", 3)
- Zhang FeiZhang FeiZhang Fei
匿名函式的定義
到此為止,每一次的函式定義中都指定了函式名,但是如果能不指定函式名就更方便了。因為即使沒有函式名,只要將函式體作為引數來傳遞或賦值給變數之後,該函式例項也就能確定了。這類函式稱為匿名函式(anonymous function),以“引數表 => 函式體”格式來定義。例如可以用如下形式來定義取得字串長度的函式。
- scala> (s:String) => s.length
如果僅這樣定義的話,該語句結束後該函式就消失了,為了能夠持續使用該函式就需要,或者持續定義該函式並適用他,或者將他賦值給變數,或者將他作為引數傳給別的函式。
- scala> ((s:String) => s.length)( "Zhang Fei") //對字串直接適用函式文字
- res2: Int = 9
- scala> val ssize = (s:String) => s.length //將函式賦值給變數
- ssize: (String) => Int = <function>
- scala> ssize("Zhang Fei") //用變數來呼叫函式
- res3: Int = 9
- scala> List("Zhang ", "Fei").map((s:String) => s.length) //對於列表每一專案都適用同一函式文字
- res4: List[Int] = List(6, 3)
- scala> List("Zhang ", "Fei").map(ssize) //對於列表每一專案都適用同一函式變數
- res5: List[Int] = List(6, 3)
上述最後兩個例子中使用了map函式,他對列表中的每一專案都適用作為引數傳入的函式之後將適用結果作為列表返回。函式則是由函式文字(s:String) => s.length或函式變數ssize來指定的。這也是閉包的一個例子,在Scala中用函式來定義閉包。任意的函式都可以作為引數來傳給別的函式。
例如前面的bar函式如下所示
- def bar(s: String, n: Int): Unit = for(i <- 1 to n) print(s)
這也可以用匿名函式來定義,這次是有兩個引數且返回值是Unit的函式。
- scala> val f0 = (s:String, n:Int) => for(i <- 1 to n) print(s)
- f0: (String, Int) => Unit = <function>
這個函式中用for語句進行了n次迴圈,其實還可以改寫成如下形式。
- def bar(s: String, n: Int): Unit = 1 to n foreach {i => print(s)}
函式體中出現的{i => print(s)}就是以匿名函式形式定義的閉包。1 to n是1.to(n)的簡化形式,然後將閉包作為引數傳遞給剛建立的Range物件的foreach方法(引數i在閉包的函式體中並沒有被使用,僅是為了語法需要)。
在表示式中作為佔位符的下劃線
實際上,Scala中備有比匿名函式更簡潔的描述方式。
如下所示,對於“(s:String) => s.length”來說,可以用“_”以“( _:String).length”形式來描述。還有可以用“(_:Int)+(_:Int)”來定義型別為“(Int, Int) => Int”的加法表示式。
- scala> ((_:String).length)("abcde")
- res6: Int = 5
- scala> ((_:Int)+(_:Int))(3, 4)
- res7: Int = 7
- scala> ((_:String).length + (_:Int)) ("abc", 4)
- res8: Int = 7
部分函式的定義
Scala中不僅可以用到現在所看到的式子來定義,還可以通過將具體的例項一排排列出後,用類似於數學中學到的映像圖的形式來描述。聲明瞭“f1:A=>B”之後可以認為是定義了將型別A映像為型別B的函式f1。實際上這可以認為是將函式定義為類Function1[A, B]的例項(圖 6-1)。
- def f1: Symbol=>Int = {
- case 'a => 1
- case 'b => 2
- case 'c => 3
- }
- scala> f1('c)
- res9: Int = 3
- scala> f1('d)
- scala.MatchError: 'd
- at $anonfun$f1$1.apply(<console>:8)
- at $anonfun$f1$1.apply(<console>:8)
- at .<init>(<console>:10)
- at .<clinit>(<console>)
- at RequestResult$.<init>(<console>:3)
- at RequestResult$.<clinit>(<console>)
- at RequestResult$result(<console>)
- at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
- at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)...
圖 6-1定義為源值域與目標值域映像的函式
函式本來不就因該是這樣的嗎?但是問題是,如果將函式定義域中沒有的引數傳給f1函式後將會丟擲例外。為了避免這種情況在對於某一值適用函式前可以先檢查一下該值是否在定義域中。部分函式(PartialFunction)定義為我們提供了這種結構。
- def f2: PartialFunction[Symbol, Int] =
- {case 'a => 1; case 'b => 2; case 'c => 3}
- scala> for(s <- List('a, 'b, 'c, 'd)){ if( f2.isDefinedAt(s) ) println( f2(s) ) }
- 1
- 2
- 3
用部分函式定義了f2:A=>B函式之後,就可以在適用函式前先使用isDefinedAt(x:A)方法來確定定義域
了。所謂的部分函式就是,對於反應源值域到目標值域的對映的函式f:A=>B,不一定存在對應於x<-A的f(x)。反過來如果對於任意的x<-A都存在f(x)的話,那f就稱為全函式。
Scala中方法和函式的關係
Scala即是純面嚮物件語言又是函式式語言,給人一種朦朧的感覺。所謂的純面向物件就是所有的語言元素都是作為物件來處理的。各個物件所持有的屬性不管是數還是字串還是陣列還是Person等例項都是物件。
因此,當然函式也是物件。實際上函式f: (ArgType1,...ArgTypeN)=>ReturnTyp是以類FunctionN[ArgType1,..., ArgTypeN, ReturnType]的例項形式被定義的。N是表示引數個數的正整數。如果是1個引數的話則是Function1[ArgType1, ReturnType]。
- def double(n:Int):Int = n * 2
上述函式基本上與下述定義是等同的。
- object double extends Function1[Int, Int] {
- def apply(n: Int): Int = n * 2
- }
- scala> double(10)
- res1: Int = 20
那麼各個物件的方法也可以稱得上物件嗎?作為測試,試著將MyFunctions物件的方法綁定於變數。
- scala> val f1 = MyFunctions.foo
- <console>:8: error: missing arguments for method foo in object MyFunctions;
- follow this method with `_' if you want to treat it as a partially applied funct
- ion
- val f1 = MyFunctions.foo
看來光是方法原樣是不能作為函式物件來處理的。實際上只要將方法簡單地轉換一下就可以作為物件來使用了。在方法名後空一格加上“_”就可以了。
- scala> val f1 = MyFunctions.foo _
- f1: (String, Int) => Int = <function>
- scala> f1("abcde", 3)
- res13: Int = 15
這樣處理之後,我們就可以明白物件的方法也可以像屬性一樣作為物件來統一處理了。Scala語言在這一點上可以說比Smalltalk那種純面嚮物件語言還貫徹了面向物件的思想。
高階函式和延遲評估引數
因為Scala的函式是物件,所以不要做什麼特殊處理只要將他作為引數傳給別的函式就自然而然地成為使用高階函數了。函式將別的函式作為引數來使用,所以稱之為高階函式。這時被傳遞的函式就稱為閉包。
用於List統一操作的函式群就是高階函式的典型例。下面的foreach函式,接受了以()或{}形式定義的閉包作為引數,然後將其逐一適用於接受者列表的所有元素。
- scala> val list = List("Scala", "is", "functional", "language")
- list: List[java.lang.String] = List(Scala, is, functional, language)
- scala> list.foreach { e => println(e) }
- Scala
- is
- functional
- language
對於同一列表list適用map函式後,對於列表list的所有元素適用s => s + “!”函式後將適用結果以列表的形式返回。這裡用空格代替了呼叫方法的“.”,然後用( _ + “!”)替代(s => s + “!”)也是可以的。
- scala> list map(s => s + "!")
- res15: List[java.lang.String] = List(Scala!, is!, functional!, language!)
- scala> list map( _ + "!")
- res16: List[java.lang.String] = List(Scala!, is!, functional!, language!)
進一步,Scala中除了有f1(p1:T1)這種通常的“基於值的引數傳遞(by value parameter)”,還有表示為f2(p2 => T2)的“基於名稱的引數傳遞(by name parameter)”,後者用於引數的延時評估。將這個結構和高階函式相混合後,就可以簡單地定義新的語言控制結構了。下面是新語言結構MyWhile的定義和使用例。
- def MyWhile (p: => Boolean) (s: => Unit) {
- if (p) { s ; MyWhile( p )( s ) }
- }
- scala> var i: Int = 0
- i: Int = 0
- scala> MyWhile(i < 3) {i=i+1; print("World ") }
- World World World
- scala> MyWhile(true) {print(“World is unlimited”) }
- 無限迴圈
像這樣充分利用了函式式語言的特點之後,我們會驚奇地發現像定義DSL(特定領域語言)那樣進行語言的擴充套件是多麼的容易和自由。
模式匹配
Scala的case語句非常強大,可以處理任何型別的物件。mach{}內部列出了case 模式 => 語句。為了確保覆蓋性可以在末尾加上 _。
- scala> val value: Any = "string"
- value: Any = string
- scala> value match {
- | case null => println("null!")
- | case i: Int => println("Int: " + i)
- | case s: String => println("String: " + s)
- | case _ => println("Others")
- | }
- String: string
這次匹配一下Person類的物件。
- scala> class Person(name:String)
- defined class Person
- scala> val value : Any = new Person("Zhang Fei")
- value: Any = [email protected]
- scala> value match {
- | case null => println("null!")
- | case i: Int => println("Int: " + i)
- | case s: String => println("String: " + s)
- | case _ => println("Others")
- | }
- Others
Case類
在Scala中模式匹配的不僅是物件,物件的屬性和型別等也可以作為模式來匹配。
例如,假設想匹配Person類,一般情況下最多就是指定“_ : Person”來匹配屬於Person類的物件了。
- scala> val value : Any = new Person("Zhang Fei")
- value: Any = [email protected]
- scala> value match {
- | case _ : Person => println("person: who")
- | case _ => println("others: what")
- | }
- person: who
不過如果使用了Case類之後,物件內的公有屬性變得也可以匹配了。定義類時只要把“class”換成“case class”之後,編譯器就會自動定義和生成同名的單例物件。並且在該單例物件中自動定義了返回該類例項的apply方法,以及返回以建構函式的引數為引數的Some型別(範型)物件的unapply(或unapplySeq)方法。並且,還自動定義了equals、hashCode和toString方法。
定義apply方法的效果是,只要定義好某個Case類之後,就可以用“類名(引數列表)”的形式來建立物件了。定義unapply方法後的效果是,可以在case語句中以Case類的建構函式的引數(物件屬性)來作為匹配目標了。
- scala> case class Person(name:String) //定義Case類Person
- defined class Person
- scala> val value : Any = Person("Zhang Fei") //不用new就可以建立物件
- value: Any = Person(Zhang Fei)
- scala> value match {
- | case Person(ns) => println("person:" + ns) //可以將Person的屬性作為匹配目標
- | case _ => println("others: what")
- | }
- person:Zhang Fei //Person的屬性name將會被抽取出來
下面是將將整數N(v)、Add(l, r)和Mult(l, r)組合後來變現四則運算Term。由於是以case形式定義的類,請注意一下在建立Term物件時,不用new就可以直接呼叫N(5)、Add(…)、Mult(…)實現了。如此使用Scala的模式匹配功能後就可以很方便地實現物件的解析工作了。
- abstract class Term
- case class N (v :Int) extends Term
- case class Add(l :Term, r :Term) extends Term
- case class Mult(l :Term, r :Term) extends Term
- def eval(t :Term) :Int = t match {
- case N (v) => v
- case Add(l, r) => eval(l) + eval(r)
- case Mult(l, r) => eval(l) * eval(r)
- }
- scala> eval(Mult(N (5), Add(N (3), N (4))))
- res7:Int = 35 // 5 * (3 + 4)
附帶說一下,上述的Term類可以認為是作為N、Add和Mult類的抽象資料型別來定義的。
將模式匹配與for語句組合
下面就看一下將模式匹配與for語句組合在一起的技巧。
- scala> val list = List((1, "a"), (2, "b"), (3, "c"), (1, "z"), (1, "a"))
- list: List[(Int, java.lang.String)] = List((1,a), (2,b), (3,c), (1,z), (1,a))
這時在<-前面寫的是像(1, x)一樣的模板。
- scala> for( (1, x) <- list ) yield (1, x)
- res6: List[(Int, java.lang.String)] = List((1,a), (1,z), (1,a))
而且非常令人驚奇的是<-前面沒有變數也是可以的。在<-之前寫上(1, “a”)之後,for語句也可以正常地迴圈並且正確地返回了兩個元素。
- scala> for( (1, "a") <- list ) yield (1, "a")
- res7: List[(Int, java.lang.String)] = List((1,a), (1,a))
還有在使用Option[T]類來避免判斷null的情況下,傳入List[Option[T]]型別的列表時,不用顯示的判斷是否是Some還是None就可以一下子返回正確的結果了。
- scala> val list = List(Some(1), None, Some(3), None, Some(5))
- list: List[Option[Int]] = List(Some(1), None, Some(3), None, Some(5))
- scala> for(Some(v) <- list) println(v)
- 1
- 3
- 5
接著用以下的例子看一下組合模式匹配和for語句之後所產生的威力。
- scala> val list = List(1, "two", Some(3), 4, "five", 6.0, 7)
- list: List[Any] = List(1, two, Some(3), 4, five, 6.0, 7)
對上述例表中的元素物件型別進行判別後再分類一下吧。模式匹配裡不僅可以使用值來作為模式,從下例可知模式還具有對Some(x)形式中的x也起作用的靈活性。
- for(x <- list){ x match{
- case x:Int => println("integer " + x)
- case x:String => println("string " + x)
- case Some(x) => println("some " + x)
- case _ => println("else " + x)
- } }
- scala> for(x <- list){ x match{
- | case x:Int => println("integer " + x)
- | case x:String => println("string " + x)
- | case Some(x) => println("some " + x)
- | case _ => println("else " + x)
- | } }
- integer 1
- string two
- some 3
- integer 4
- string five
- else 6.0
- integer 7
結束語
看了本文之後大家覺得怎麼樣呀?應該享受了Scala所具備的,將面向物件式和函式式語言功能充分融合的能力,以及高階函式和模式匹配功能了吧。
Scala語法的初步介紹就到本講為止了,接下來的講座將介紹一下Scala語言更深入的部分。包括隱式轉換、範型和單子等有趣的話題。