在 Kotlin 中“實現”trait/型別類
本文原發於我的個人部落格:https://hltj.me/kotlin/2020/01/11/kotlin-trait-typeclass.html。本副本只用於圖靈社群,禁止第三方轉載。
trait 與型別類都是什麼
trait 與型別類(type class)分別是 Rust 與 Haskell 語言中的概念,用於特設多型(ad-hoc polymorphism)、函數語言程式設計等方面。
值得一提的是雖然英文都是“trait”, Scala 的特質跟 Rust 的 trait [^1] 卻並不相同。 Scala 的特質相當於 Kotlin 與 Java 8+ 的介面,能實現子型別多型;而 Rust 的 trait 更類似於 Swift 的協議與 Haskell 的型別類,能實現特設多型。簡單來說,trait 應同時具備以下三項能力[^2]:
- 定義“介面”並可提供預設實現
- 用作泛型約束
- 給既有型別增加功能
Haskell 的型別類不僅同時具備這三項能力,還能定義函數語言程式設計中非常重要的 Functor、Applicative、Monad 等。 當然這是廢話,因為它們在 Haskell 中本來就是型別類?。 實際上這也不是 trait 與型別類的差異,能否支援 Functor 等的關鍵在於語言的泛型引數能否支援型別構造器(或者說語言能否支援高階型別)。
[^1]: trait:Scala 中文社群傾向於譯為“特質”,Rust 中文社群傾向於不譯。
[^2]: 按說“介面”不支援預設實現也可實現 Rust/Haskell 式的特設多型,但其易用性與表現力都要大打折扣。
在 Kotlin 中尋求對應
在 Kotlin 中並沒有同時具備這三項能力的對應,只有分別提供三項能力的特性。 其中 Kotlin 的介面同時具備前兩項能力。
定義“介面”並可提供預設實現
例如,定義一個帶有預設實現的介面:
interface WithDescription {
val description: String get() = "The description of $this"
}
Kotlin 的介面中可以定義屬性與方法,二者都可以有預設實現,簡便起見,示例中用了具有預設實現的屬性。它可以這麼用:
class Foo: WithDescription {
// Foo 類為 description 屬性提供了自己的實現
override val description = "This is a Foo object"
}
// 物件 Bar 的 description 屬性採用預設實現
object Bar: WithDescription
println(Foo().description)
println(Bar.description)
在 Kotlin REPL 中執行會得到類似這樣的輸出:
This is a Foo object
The description of Line_7$Bar@5bf4764d
用作泛型約束
接下來還可以將之前定義的 WithDescription
介面用在泛型函式、泛型類或者其他泛型介面中作為泛型約束,例如:
fun <T : WithDescription> T.printDescription() = println(description)
在 REPL 中執行:
>>> Bar.printDescription()
The description of Line_7$Bar@5bf4764d
遺憾的是,在 Kotlin 中不能給既有型別(類或介面)實現新的介面,比如不能為 Boolean
或者 Iterable
實現 WithDescription
。
即介面不具備第三項能力,因此它不是 trait/型別類。
給既有型別增加功能
在 Kotlin 中給既有型別增加功能的方式是擴充套件,可以給任何既有型別宣告擴充套件函式與擴充套件屬性。例如可以分別給 Int
與 String
實現二者間的乘法操作符函式:
operator fun Int.times(s: String) = s.repeat(this)
operator fun String.times(n: Int) = repeat(n)
於是就可以像 Python/Ruby 那樣使用了:
>>> "Hello" * 3
res11: kotlin.String = HelloHelloHello
>>> 5 * "漢字"
res12: kotlin.String = 漢字漢字漢字漢字漢字
在 Kotlin 中“實現”trait/型別類
如上文所述,Kotlin 分別用介面與擴充套件兩個不同特性提供了 trait/型別類的三項能力,因此在 Kotlin 中沒有其直接對應。 那麼如果把兩個特性以某種方式結合起來,是不是就可以“實現”trait/型別類了?——還別說,真就可以! Arrow 中的型別類就是這麼實現的。
我們繼續以 WithDescription
為例,不同的是,這回要這麼宣告:
interface WithDescription<T> {
val T.description get() = "The description of $this"
}
這裡利用了分發接收者可以子類化、擴充套件接收者靜態解析的特性,可以為任何既有型別新增實現。例如分別為 Char
、String
實現如下:
object CharWithDescription : WithDescription<Char> {
override val Char.description get() = "${this.category} $this"
}
// 採用預設實現
object StringWithDescription: WithDescription<String>
不過使用時會麻煩一點,需要藉助 run()
或者 with()
這樣的作用域函式在相應上下文中執行:
println(StringWithDescription.run { "hello".description })
with(CharWithDescription) {
println('a'.description)
}
在 REPL 中執行的輸出如下:
The description of hello
LOWERCASE_LETTER a
用作泛型約束也不成問題:
fun <T, Ctx : WithDescription<T>> Ctx.printDescription(t: T) = println(t.description)
StringWithDescription.run {
CharWithDescription.run {
printDescription("Kotlin")
printDescription('①')
}
}
這裡實現的 printDescription()
與上文的函式簽名不同,因為接收者型別用於實現基於作用域上下文的泛型約束了,這也是利用介面、擴充套件、子型別多型以及作用域函式這些特性來“實現”trait/型別類的關鍵所在。
當然,如果仍然希望目標型別(如例中的 Char
、String
)作為 printDescription
的接收者,只要將其接收者與引數互換即可:
fun <T, Ctx : WithDescription<T>> T.printDescription(ctx: Ctx) = ctx.run {
println(description)
}
"hltj.me".printDescription(StringWithDescription)
上述兩種方式中提供泛型約束的上下文要麼佔用了函式的擴充套件接收者、要麼佔用了函式引數。實際上還有一種方式——佔用分發接收者,顯然只要在 WithDescription
內宣告 printDescription()
就可以了。
不過我們這裡要假設 printDescription()
是自己定義的函式,而 WithDescription
是無法修改的既有型別,那麼還能做到嗎?——當然不成問題!只要用一個新介面繼承 WithDescription
就可以了:
interface WithDescriptionAndItsPrinter<T>: WithDescription<T> {
fun T.printDescription() = println(description)
}
object StringWithDescriptionAndItsPrinter: WithDescriptionAndItsPrinter<String>
object CharWithDescriptionAndItsPrinter:
WithDescriptionAndItsPrinter<Char>, WithDescription<Char> by CharWithDescription
StringWithDescriptionAndItsPrinter.run {
CharWithDescriptionAndItsPrinter.run {
"hltj.me".printDescription()
'★'.printDescription()
}
}
將三種方式放一起對比會更直觀:
// 方式 1
StringWithDescription.run {
printDescription("hltj.me")
}
// 方式 2
"hltj.me".printDescription(StringWithDescription)
// 方式 3
interface WithDescriptionAndItsPrinter { /*……*/ }
object StringWithDescriptionAndItsPrinter: WithDescriptionAndItsPrinter<String>
StringWithDescriptionAndItsPrinter.run {
"hltj.me".printDescription()
}
第三種方式的優點是提供泛型約束的上下文既不佔用擴充套件接收者也不佔用引數,但其代價是需要為每個用到的目標型別(如例中的 Char
、String
)提供新介面(如例中的 WithDescriptionAndItsPrinter<T>
)的相應實現,並且依然需要藉助作用域函式 run()
或 with()
。
因此通常採用前兩種方式即可,但是如果要自定義操作符函式或者中綴函式時就只能採用第三種方式了,例如:
interface DescriptionMultiplier<T>: WithDescription<T> {
infix fun T.rep(n: Int) = (1..n).joinToString { description }
operator fun T.times(n: Int) = this rep n
}
object CharDescriptionMultiplier:
DescriptionMultiplier<Char>, WithDescription<Char> by CharWithDescription
println(object : DescriptionMultiplier<String> {}.run { "hltj.me" rep 2 })
println(CharDescriptionMultiplier.run { 'A' * 3 })
在 REPL 中執行的輸出為:
The description of hltj.me, The description of hltj.me
UPPERCASE_LETTER A, UPPERCASE_LETTER A, UPPERCASE_LETTER A
擴充套件與成員的優先順序
我們知道,在 Kotlin 中擴充套件與成員衝突時總是取成員。 但是在使用基於作用域上下文的泛型約束時卻並非如此,例如:
interface WithLength<T> {
val T.length: Int
}
object StringWithFakeLength: WithLength<String> {
override val String.length get() = 128
}
fun <T, U: WithLength<T>> U.printLength(t: T) = println(t.length)
StringWithFakeLength.run {
printLength("hltj.me")
}
在 REPL 中執行輸出是 128
,表明 printLenth()
取到的 length
是 StringWithFakeLength
中定義的擴充套件屬性而不是 String
自身的屬性。因此使用時需要特別注意。此外對於 Any 的三個成員 toString()
、hashCode()
、equals()
會始終呼叫成員函式,即便在泛型約束上下文中宣告瞭具有相同簽名的擴充套件函式也是一樣。
“實現”Functor 等
按照上文介紹的方式,我們可以輕鬆實現 Show
、Eq
、Ord
等簡單型別類,無需贅述。
但是如果要實現 Functor
、Applicative
、Monad
等卻會遇到問題。
以 Functor
為例,按說要這麼定義:
interface Functor<C<*>> {
fun <T, R> C<T>.fmap(f: (T) -> R): C<T>
}
但遺憾的是上述程式碼無法通過編譯,因為 Kotlin 目前不支援高階型別,在泛型引數中用 C<*>
表示型別構造器只是假想的語法 。
因此,需要有一種方式來變通。按 Arrow 的方式引入 Kind
介面來表示:
interface Kind<out F, out A>
interface Functor<F> {
fun <T, R> Kind<F, T>.fmap(f: (T) -> R): Kind<F, R>
}
然後寫一個標記類,讓具體型別作為 Kind<標記類, T>
的實現類。再定義一個由 Kind<標記類, T>
向具體型別轉換的擴充套件函式 fix()
,以便在具體實現中使用。
例如:
class ForMaybe private constructor()
sealed class Maybe<out T> : Kind<ForMaybe, T> {
object `Nothing#` : Maybe<Nothing>() {
override fun toString(): String = "Nothing#"
}
data class Just<out T>(val value: T) : Maybe<T>()
}
fun <T> Kind<ForMaybe, T>.fix(): Maybe<T> = this as Maybe<T>
這樣就可以為 Maybe
實現 Functor<ForMaybe>
了:
object MaybeFunctor : Functor<ForMaybe> {
override fun <T, R> Kind<ForMaybe, T>.fmap(f: (T) -> R): Maybe<R> = when (val maybe = fix()) {
is Maybe.Just -> Maybe.Just(f(maybe.value))
else -> Maybe.`Nothing#`
}
}
fun main() = with(MaybeFunctor) {
println(Maybe.Just(5).fmap { it + 1 })
println(Maybe.`Nothing#`.fmap { x: Int -> x + 1 })
}
可以看出這種實現方式會有明顯的侷限性:只能為 Arrow 中定義的型別或者按照 Arrow 方式實現的既有型別實現 Functor
、Applicative
、Monad
等接受型別構造器作為泛型引數的“型別類”。
好在 Arrow 已經自帶了大量有用的型別,很多場景都夠用。
需要注意的是這段程式碼無法在當前版本(1.3.61)的 Kotlin REPL 中執行,需要放在普通的 Kotlin 檔案中編譯執行。
Arrow
Arrow(按其官網寫作 Λrrow)是 Kotlin 標準庫的函式式“伴侶”。目前主要以下四套件:
- Arrow Core 提供了核心的資料型別與型別類。
- Arrow FX 是函式式副作用庫,提供了
do
-表示法/Monad 推導風格的 DSL。 - Arrow Optics 用於在 Kotlin 中處理不可變資料模型。
- Arrow Meta 是 Kotlin 編譯器與 IDE 的函式式“伴侶”。
此外還有若干套件/特性還在孵化中。 關於 Arrow 整體與模式的介紹也在 Arrow Core 的文件中,其中 Functional Programming Glossary 提供了一些使用 Arrow 進行函數語言程式設計的背景知識可供參考。
一點意外
在嘗試寫這些示例時意外發現了一個會導致當前版本的 Kotlin JVM 編譯器拋異常的 bug,最小重現程式碼如下:
interface WithIntId<T> {
val T.intId get() = 1
}
object BooleanWithIntId : WithIntId<Boolean>
val x = BooleanWithIntId.run {
true.intId
}
隻影響 Kotlin JVM 編譯器,Kotlin JS 與 Kotlin Native 都不存在這個問題。 查了下 YouTrack,看起來是個已知 bug。 不過文中的其他示例程式碼都能正常編譯執行,儘可放心。
相關文章
- 在例項中呼叫 Invoke 型別的類型別
- rust trait 關聯型別和泛型的區別RustAI型別泛型
- 使用Spring Boot、Kotlin和OpenFeign實現型別安全API測試Spring BootKotlin型別API
- Kotlin教程(五)型別Kotlin型別
- trait 和型別的方法同名的例子AI型別
- 實現TypeScript中的互斥型別TypeScript型別
- java中介面多個實現類,如何指定實現類,根據子類型別選擇實現方法Java型別
- Python在類中實現swith case功能Python
- 教你如何完全解析Kotlin中的型別系統Kotlin型別
- 淺談 Trait 類AI
- Rust 中的Box型別實現堆分配Rust型別
- Kotlin 基礎 - 資料型別Kotlin資料型別
- Kotlin_lesson_01_基本型別Kotlin型別
- Kotlin中的泛型Kotlin泛型
- 利用 trait 簡易 Facade 實現AI
- [譯]Kotlin泛型中何時該用型別形參約束?Kotlin泛型型別
- [kotlin]帶分類的RecyclerView通用實現新思路KotlinView
- Kotlin的基本語法和型別Kotlin型別
- 【譯】在非泛型類中建立泛型方法泛型
- 在鴻蒙中實現類似瀑布流效果鴻蒙
- Kotlin可空型別與非空型別以及`lateinit` 的作用Kotlin型別
- Dive Into Kotlin(四):為什麼 Kotlin 的根型別是「Any?」Kotlin型別
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)Kotlin泛型
- Kotlin的獨門祕籍Reified實化型別引數(下篇)Kotlin型別
- Kotlin 泛型中的 in 和 outKotlin泛型
- Kotlin + Netty 在 Android 上實現 Socket 的服務端KotlinNettyAndroid服務端
- PHP弱型別在實戰中導致的漏洞總結PHP型別
- [譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)Kotlin型別
- 實現Nest中引數的聯合型別校驗型別
- 列舉型別在JPA中的使用型別
- PHP trait 特性在 Laravel 中的使用個人心得PHPAILaravel
- 深度解析:在 React 中實現類似 Vue 的 KeepAlive 元件ReactVue元件
- 深入解析多型和方法呼叫在JVM中的實現多型JVM
- 為什麼資料庫表的int型別欄位對映到實體類中要使用Integer型別,而不是int型別?...資料庫型別
- 用trait實現簡單的依賴注入AI依賴注入
- C#引用型別和值型別在堆、棧中的儲存C#型別
- Kotlin實戰【五】Kotlin中的異常Kotlin
- 【SpringBoot實戰開發】第2講Kotlin型別系統與空安全Spring BootKotlin型別