標籤:

Data class 是好東西,Kotlin 就差一個 pattern matching 了

我注意到在我前些天的一篇回答的評論里,Swift 和 Scala 都被拿來和 Kotlin 做比較。

我於是忽然想寫一點沒什麼技術水平、也沒什麼內容結構的隨筆,談談一些關於 data class 的一些有意思的地方。

我寫 Swift 時也注意到,Swift 里的 enum 中每一項是可以有參數的,亦即「associated values」。例如:

enum Gender {n case malen case femalen case other(description: String)n}n

於是寫 Kotlin 的時候,我就在想,為什麼 Kotlin 的 enum 不能帶參數呢…… 後來我才意識到,其實「帶 associated values 的 enum」實際上可以用 sealed class 的語法來等同:

sealed class Gender {n object Male: Gender()n object Female: Gender()n data class Other(val description: String): Gender()n}n

Sealed class 和 data class 這兩個東西,用在一起真是蠻合適呢。其實這個例子里,不加「sealed」和「data」兩個修飾符,也能編譯通過。但是,加上這兩個修飾符,似乎更能表達類似於 enum 的語義。為什麼呢?

在 Kotlin 里,sealed class 也是為了協助 when 語句的 exhaustiveness check 的;也就是說,對於一個 sealed class,如果 when 已經處理了它的所有已知子類,那就不再需要 else 分支。事實上,Sealed class 這個概念在 Scala 里也有完全相同的存在——相似地,它的主要用途是協助 pattern matching 的 exhaustiveness check。

而 Scala 里也有著和 data class 相似的存在,那就是 case class。它們同樣用於表示「僅僅用於組織數據的類」,也就是類似於「帶了 tags 的 tuple」;並且,它們同樣提供了符合直覺的 equals 方法的實現。

然而,在 Scala 里,正如「case class」這個名字所暗示,其一個很大的用途是在 pattern matching 中。在 Scala 中,假設有

case class Bar(noun: String, verb: String)n

那麼就可以對變數 foo: Bar 做

foo match {n case Bar(_, "藥丸") => "後面有藥丸"n case Bar("青果", predicate) => "青果" + predicate + "了"n case _ => "啥都沒有"n}n

所以:Kotlin 不提供 pattern matching 簡直就是反人類!

Improved Pattern Matching in Kotlin 提供了一些增強 when 語句的奇技淫巧。當然,這樣的奇技淫巧的代價,就是喪失 exhaustiveness check 的功能——因為 when 語句的 exhaustiveness check 非常弱,僅僅能識別「is XXX」這樣的條件。

利用類似的奇蹟淫巧,再加上更骯髒的反射,我們就能對 Kotlin 的 data class 做類似的事情——至少實現上面那段 Scala 中那樣,對 foo 中的屬性的值做 matching。這裡利用的是,Kotlin 對於 data class 會生成 componentN() 方法 。

「奇技淫巧」所需要的一點 bolterplating:

object any // 因為沒法直接用 _nnclass MatchDataClass<T: Any>(private val kClass: KClass<T>, private val params: Array<out Any>) {n init {n if (!kClass.isData) { throw IllegalArgumentException("Not a data class!") }n }n operator fun contains(input: Any): Boolean { // 實現「in」操作符n return if (!kClass.isInstance(input)) false else (n params.mapIndexed { index, criteria ->n (criteria is any) || (kClass.java.getMethod("component" + (index + 1)).invoke(input) == criteria)n }.all { it }n )n }n}nninline fun <reified T: Any> match(vararg params: Any) = MatchDataClass(T::class, params)n

於是,我們現在就可以寫出跟上面的 Scala 代碼有點像的 Kotlin 代碼來:

when (foo) {n in match<Bar>(any, "藥丸") -> "後面有藥丸"n in match<Bar>("青果", any) -> { val (_, predicate) = foo; "青果" + predicate + "了" }n else -> "啥都沒有"n}n

注意到,第二個 case 的代碼比較噁心。這是因為 Scala 能夠在 matching 的同時創建變數並賦值,而 Kotlin 再怎麼 hack 也無法做到。幸而 Kotlin 還算支持 destructing。

當然,這樣的寫法有很多問題:

  • 用了反射,這樣不僅慢,而且很臟
  • 編譯器不會為你做類型檢查,寫出 match<Bar>(1, 2) 編譯器也不會指出錯誤
  • 同樣,exhaustiveness check 和 smart cast 就通通沒法幫你了

所以,似乎我們還是應該期待 Kotlin 提供完整的 pattern matching 支持,才能發揮 sealed class 和 data class 的最大功力呢。
推薦閱讀:

#scala#模式匹配
【太閣x周刊】第十一期:紐約線下活動預告、技術討論群熱點話題、Scala中的Typeclass模式實例、人氣文章
提議:在Dotty 中使用縮進語法
Stack monads in Scala
Scala快速入門-6-單例對象及伴生對象

TAG:Kotlin | Scala |