[Scala] TypeClass 簡介(二)--手上的工具

Scala 裡的 TypeClass 用到的工具

上一篇提到,在 Scala 裡面所謂的 TypeClass,比較像是利用手上的工具套公式來解決問題的設計模式,而不像 Haskell 一樣直接內建成程式語言的語法。

也因為如此,在這一篇裡面,我們要先來看一下到底 Scala 提供了哪些工具給我們使用。

由於這些工具是 Java 裡沒有的,所以如果不先弄清楚他們的功能的話,看到使用了 TypeClass 的 Scala 的程式的時候,很可能會根本不知道發生了什麼事,以及為什麼他會發生。XD

回到正題,在 Scala 裡面要達成 TypeClass 的話,主要靠的是 Scala 提供的以下三個功能:

  • Trait
  • Curried Function
  • Implicit Parameter

其中 Trait 的部份,可以看做 Java 裡的 Interface,雖然實際上他比 Interface 強大,不過在 TypeClass 裡的情況,Trait 的作用基本上和 Interface 一樣,所以這裡就不多介紹,只把重點放在 Java 裡沒有的 Curried Function 和 Implicit Parameter 上面。

Curried Function

Note

在 Scala 裡,嚴格說來 function 和 method 是完全不同的東西,不過這篇只是簡介,所以不去細分。但如果真的要玩 Scala,最好還是要搞清楚兩者的不同,不然有的時候會不懂為什麼寫出來的程式碼編譯器不給過。

在寫 Java / C 的時候,我們經常會把程式碼切成好幾個 method / function 來寫。我們都知道,這樣子寫出來的程式碼會比較清楚,也比較容易維護。

而在 Java / C 裡面,我們要定義一個 method 或 function 的時候,基本上會做以下的事情:

  1. 決定函式的名字
  2. 決定這個函數要有哪些參數
  3. 實作這個函數實際上執行時的程式碼

一個簡單的例子是,如果我們今天要實作一個可以計算 x 的 y 次方的函數時,我們會這樣定義:

def pow (x: Int, y: Int): Long = {

  var result: Long = 1

  for (i <- 0 until y) {
    result = result * x
  }

  result
}

然後使用的時候,我們就可以直接呼叫 pow,並給定 xy 兩個數字,來叫這個函式幫我們計算結果,並且當給定不同的 xy 值時,得出的答案也會不同:

scala> pow(2, 3)
res0: Long = 8

scala> pow(2, 4)
res1: Long = 16

scala> pow(3, 4)
res2: Long = 81

咦?這不是我們在寫程式時天天都在做的事嗎?現在大部份的語言都提供這樣子來定義函式來重複使用程式碼啊?為什麼要特別提出來講呢?

這是因為,不論是在 Java 或 C 裡面,我們的函式的參數列表都只有一個,也就是函數名稱後面的 (...) 只會有一組。但是在 Scala 裡面,函數後面的參數列表是可以有任意多組的!

什麼?參數列表可以有好幾組?這是咋回事?

沒錯,在 Scala 裡面,我們的 method / function 後面的小括號是可以有一組以上的,舉例來說,我們可以把上述的 pow 裡的 xy 放在不同的括號裡:

def pow (x: Int)(y: Int): Long = {

  var result: Long = 1

  for (i <- 0 until y) {
    result = result * x
  }

  result
}

然後呼叫的時候給他兩個括號,並把 xy 的值放在兩個不同的括號之中:

scala> pow(2)(3)
res6: Long = 8

scala> pow(2)(4)
res7: Long = 16

scala> pow(3)(4)
res8: Long = 81

看起來好像只是語法上不同而已嘛,只不過是把 pow(2, 3) 換成了 pow(2)(3) 而已啊,這樣子有什麼用嗎?只不過是多了另一個方法來做同樣的事嘛!

先別急,有趣的事在後頭呢!

不知道你有沒有想過,如果我們在呼叫 pow 的時候,只給一個括號而不是兩個括號,會發生什麼事情呢?嗯,Scala 有好用的 REPL 的說,我們直接在 REPL 裡試試看:

scala> pow(2)
<console>:9: error: missing arguments for method pow;
follow this method with `_' if you want to treat it as a partially applied function

呃,有錯誤發生,而且看不懂他在說什麼,不過好像是叫我們把一個底線放在這個 method 後面。反正死馬當活馬醫,來試試看好了:

scala> pow(2)_
res10: Int => Long = <function1>

這次成功是了,不過這次他返回的東西是……function1?

沒錯,當你呼叫 pow(2) 的時候,實際上是得到了另一個 function / method,所以 res10 是個實實在在的 function,我們可以像呼叫 function 一樣呼叫他:

scala> res10(3)
res12: Long = 8

scala> res10(4)
res13: Long = 16

scala> res10(5)
res14: Long = 32

可以看到,現在我們的 res10 其實就變成了一個計算 2 的 N 次方的函式了。

話說回來,在這個例子裡面,我們實在看不太出來這樣做有什麼好處,或 Curried Function 實際上有什麼功用,不過因為 TypeClass 會用到 Scala 裡的這個功能,我們還是要先了解一下才行。

Implicit Parameter

在 Scala 裡定義 Method / Function 的時候還有另一個有趣,而且 Java 裡沒有的東西,叫做 Implicit Parameter,他的作法很簡單,就是在宣告 Method 的時候,在參數列表的最前面加上一個 implicit 的關鍵字,像下面一樣:

def greeting(implicit name: String) {
  println ("Hello, " + name)
}

然後在使用的時候,我們當然還是可以像呼叫一邊的函式一樣呼叫它:

scala> greeting("BrianHsu")
Hello, BrianHsu

但真正有趣的,是如果我們的函數列表裡面有 implicit 這個關鍵字的話,我們可以用下面的方法來提供預設值!

scala> implicit val name = "Miho"
name: String = Miho

scala> greeting
Hello, Miho

scala> implicit val name = "Hsu"
name: String = Hsu

scala> greeting
Hello, Hsu

你可以看到,當我們給定了 implicit 的預設值的時候,我們在呼叫 greeting 這個函數時,就不用傳遞 name 的這個參數。

這裡要注意的是,這和像下面這種直接在 method 裡定義預設值的方式,語義上是不同的:

def greeting(name = "Brian") {
  println("Hello, " + name)
}

如果你是用上面的方式來定義預設值,那這個 greeting 函式不管在哪裡被呼叫,他的預設值都是 Brian 這個字串。

但如果用 Implicit Parameter 的方式,你可以在因應不同的需求,定義不同的預設值,例如下面的例子,兩個 for 迴圈裡呼叫的 greeting,他們的預設值是不同的。

在第一迴圈裡的預設值是 Brian 這個字串,但第二個迴圈裡的預設值則是 Miho 這個字串:

def greeting(implicit name: String) {
  println ("Hello, " + name)
}

for (i <- 0 until 10) {
  implicit val name = "Brian"
  greeting
}

for (i <- 0 until 10) {
  implicit val name = "Miho"
  greeting
}

換句話說,當你把函式的參數列表宣告成 implicit 的時候,實際上是把「決定參數的預設值」這件事的權力,下放給了你的函數的使用者。雖然你也可以提供自己的 implicit 預設值,但是否要使用這個預設值的權力,卻是在使用者的手中。

當把 Curried Function 遇到 Implicit Parameter

上面介紹了 Curried Function 和 Implicit Parameter 之後,如果我們把兩者結合起來的話,就會發生一件有趣的事情:

object Greeting {

  implicit val count = 5

  def repeatGreeting(name: String)(implicit count: Int) {
    for (i <- 0 until count) {
      greeting("Hello, " + name)
    }
  }
}

import Greeting._

repeatGreeting("Brian")

在這邊,要注意的是,當我們在第 14 行呼叫 repeatGreeting 的時候,我們並沒有去管 repeatGreeting 的第二個參數列表。

換句話說,對於使用者而言,使用 repeatGreeting 的時候,第二個參數列表裡的 count 就如同不存在似的,他只要專注於到底要向誰打招呼--但同時我們又保留了一定的彈性,讓使用者可以隨時更動打招呼的次數的預設值。

這也是為什麼雖然在 Scala 的標準函式庫裡,我們天天在用的 List.map 實際上長的像這樣:

def map[B, That](f: (A)  B)(implicit bf: CanBuildFrom[List[A], B, That]): That

但我們使用的時候,卻像下面這樣:

List(1, 2, 3, 4).map(x => x + 1)

我們只提供了第一組參數裡的 f,而完全不去管後面的那個 bf 是什麼鬼,就是由於 Scala 的標準函式庫已經提供了 bf 的預設值的關係。

小結

在這篇文章裡面,我們稍微看了一下 Scala 提供的一些程式語言上的工具。

但這些工具在達成 TypeClass 的時候,扮演了什麼樣的角色,而 TypeClass 又到底是啥鬼,為什麼可以解決我們第一篇提出的問題呢?請待下回分解。

回響