Kotlin Context Receivers 的那一兩件事情
Context Receivers 是甚麼個玩意?
前言
不知道各位有沒有遇過一種情況是這樣的,我這有兩個 interface 分別長這樣;
interface A {
val a: Int
}
interface B {
val b: Int
}
那使用上會是這樣
fun method(a: A, b: B) {
println("I want to combine this those parameter ${a.a} ${b.b}")
}
覺得這樣要把所有的 parameter 帶進來有一點麻煩,理想上是寫這樣:
fun method() {
println("I want to combine this three parameter $a $b")
}
但我要怎麼知道,a b c 三個項目是打哪來的呢?
這個時候我們可能會先想到 Extension
fun A.method() {
println("I want to combine this three parameter $a $b")
}
但是這個 Extension 只是針對 A 的,我如果要把 B 的 b 傳進來,那我還是得傳入 B。
這裡我想到一個方法:
interface A {
val a: Int
}
interface B {
val b: Int
fun A.method() {
println("I want to combine this three parameter $a $b")
}
}
在語法上的陳述是,我希望在有 B object 的情況下可以外掛 A 的 method。
只是實際寫要怎麼寫呢?
fun main() {
val a: A = AImpl()
val b: B = BImpl()
b ... // How should I write this ?
a.method()
}
Kotlin 在這裡提供一個 inline function 叫做 with
fun main() {
val a: A = AImpl()
val b: B = BImpl()
with(b) {
a.method()
}
}
這樣就可以做到在限定在某個 object 底下使用某個 Extension 。
只是我這樣寫的時候被我同事看到,他露出了困惑的表情,然後問:「你這是在寫三小?」
況且,如果我們今天面對的是這種情況:
interface A {
val a: Int
}
interface B {
val b: Int
}
interface C {
val c: Int
}
fun method(a: A, b: B, c: C) {
println("I want to combine this three parameter ${a.a} ${b.b} ${c.c}")
}
那像是這情況,就算在 interface 掛上 A 的 extension ,但你一樣也是得面對 要傳入 C instance 的問題。
什麼是 Receiver ?
若要了解 Context Receivers 為何物,勢必要先了解在 Kotlin 裡頭的 Receiver 為何物?
恩,照理說如果有寫 Kotlin ,而且還寫上一段時間,應該有很大的機率看過,或者是使用過 Receiver 了,當然我們最常見的 case ,就是在使用 Extension 或者是 lambda 了,這裡用 Extension 舉個例子:
fun main(){
"This is a String.".extensionFun()
}
private fun String.extensionFun() {
println(this)
}
Emm … 有意思的地方是,誰是 receiver ,具體又 receive 了甚麼東西呢?
這一部分其實可以在 kotlin source code 裏頭一看究竟,用我們剛才用到的 with function 來看。
我們可以看到 with 傳入的東西叫做 receiver ,也就是說如果我把上面的 Code 改成這樣:
fun main() {
with("This is a String.") {
extensionFun()
}
}
private fun String.extensionFun() {
println(this)
}
那可以看到 "This is a String" 這個 String 就是 receiver ,而他 receive 了那個 block。
換句話說,可以把上頭的 extensionFun 解釋為:要讓一個 String 作為 Receiver 去接收名為 extension 的這個 Block。
這樣的好處就是可以在寫 Code 時可以不用這麼專注在整體的結構上頭。
那麼 Context Receivers 又是啥?
知道 Receiver 是甚麼玩意之後,那 context receivers 解釋起來就不難了, receivers 是 receiver 的複數, context 我們可以解釋成是帶有某些條件的意境(在語言上就是語境,也就是上下文,context 這個在中文真的很難翻到位),也就是說 Kotlin 的 Context Receivers 就是指某個 object 或 method 帶有一個多個 Receiver 的屬性。
別廢話讓我看 Code
Context Receiver 在 Jetbrains 目前的實作上仍定調為實驗階段,所以官方目前的推薦是在可以隨便玩玩的 Project 上嘗試就好(雖然我自己在寫 Compose 的時候會有點想用他),目前也只能在 JVM 上頭被編譯,而且也不是預設開啟,所以如果說要玩,那麼就需要改一下 Gradle 的設定:
KTS:
tasks.withType<KotlinCompile>{
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers")}
Groovy:
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ["-Xcontext-receivers"]
}
不設定你會看到 IDE 凶巴巴的跟你說:
編譯不會過就算惹,IDE 也根本不知道你想幹嘛就是了。
好,那麼就來舉個例子吧:
fun addMonthAndPrintFormattedDate(calendar: Calendar, sdf: SimpleDateFormat) {
calendar.add(Calendar.MONTH, 1)
println(sdf.format(calendar.time))
}
這個方法很簡單,我們給 calendar 加上一個月,然後印出來。
那實作上會是這樣:
fun main() {
val calendar = Calendar.getInstance()
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
addMonthAndPrintFormattedDate(calendar, sdf)
addMonthAndPrintFormattedDate(calendar, sdf)
addMonthAndPrintFormattedDate(calendar, sdf)
}
那個 sdf 每次用每次都得傳入還是有點擾人,我們在傳統的方式上可能會採用把這玩意包成一個 class 的做法,或者是用 Extension 的方式。 extension 的方法就回到這篇文章一開始的問題上了。
那寫法可以改成這樣:
fun SimpleDateFormat.addMonthAndPrintFormattedDate(calendar: Calendar) {
calendar.add(Calendar.MONTH, 1) println(format(calendar.time))
}
最後會變成這樣:
fun main() {
val calendar = Calendar.getInstance()
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
with(sdf) {
addAMonthAndPrintFormattedDate(calendar)
addAMonthAndPrintFormattedDate(calendar)
addAMonthAndPrintFormattedDate(calendar)
}
}
只是有個問題是,如果仔細端看 SimpleDateFormat 的 Extension addMonthAndPrintFormattedDate 會發現,在 Calendar 上加入一個月這件事情是由 SimpleDateFormat 來做了,這樣在語境上相當的不合邏輯:「簡易日期格式工具給 Calendar 加了一個月上去並印出規格化後的日期」,日期格式工具怎麼會負責修改 Calendar 呢?
在我理解到我一個月後回來看到這段會不知道當時的我在 do 三小的時候,我默默地說了一句:「加個註解好惹」
而且每次都要傳入 calendar 也有點煩。
這也便是 Context Receivers 想要達成的目的之一:當你需要類似 SimpleDataFormat 的屬性作為 Context 的同時,省去上面攏長的變數和定義。
那其實要改也不難,就是類似這樣:
context(SimpleDateFormat)
fun addMonthAndPrintFormattedDate(calendar: Calendar) {
calendar.add(Calendar.MONTH, 1)
println(format(calendar.time))
}
那實際的 control flow 也不需要變動,按照原樣就好,或者可以改成用 run:
fun main() {
val calendar = Calendar.getInstance()
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
sdf.run {
addMonthAndPrintFormattedDate(calendar)
addMonthAndPrintFormattedDate(calendar)
addMonthAndPrintFormattedDate(calendar)
}
}
那更進一步,可以把 calender 也給加上去 context 上頭:
context(SimpleDateFormat, Calendar)
fun addMonthAndPrintFormattedDate() {
add(Calendar.MONTH, 1)
println(format(time))
}
control flow 則會變成這樣:
fun main() {
val calendar = Calendar.getInstance()
val sdf = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault())
with(sdf) {
with(calendar) {
addMonthAndPrintFormattedDate()
addMonthAndPrintFormattedDate()
addMonthAndPrintFormattedDate()
}
}
}
當然要注意到, Kotlin 畢竟也只是一碼程式語言,他還沒本事推斷出具體要使用甚麼實體,所以使用上沒加 with 或者是 run 或者是 apply 之類定義 receiver 的 method 會辨識不出來。
是說這樣的 with 和 with 會有波動拳的情況,只是在 Jetbrains 的影片有提到,未來會有更優雅的寫法。
Multiple Receivers
那有了上面的例子,我們應該可以很清楚地回答我一開始提到的問題了,當我有 A B C 三個 interface 的時候,我有沒有比較省事的寫法呢?
interface A {
val a: Int
}
interface B {
val b: Int
}
interface C {
val c: Int
}
context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
}
實際寫起來要讓他 work 會長成這樣
interface A {
val a: Int
}
interface B {
val b: Int
}
interface C {
val c: Int
}
class AImpl : A {
override val a: Int = 0
}
class BImpl : B {
override val b: Int = 0
}
class CImpl : C {
override val c: Int = 0
}
context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
}
fun main(){
val a:A = AImpl()
val b:B = BImpl()
val c:C = CImpl()
with(a){
with(b){
with(c){
method()
}
}
}
}
yep ~就是這樣。
只是在寫的過程中,應該就會發現到這會有一點問題了,上述的情況是在參數和方法名稱都不一樣的時候,那如果說我 interface A 有個 methodA ,而我 interface B也有個 methodA 時,這該怎麼辦呢?
interface A {
val a: Int
fun method1()
}
interface B {
val b: Int
fun method1()
}context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
method1()
}
其實情況跟在用巢狀的 run 中的 this 一樣,解決的方法也是一樣的 — 用 labeled
context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
this@A.method1()
this@B.method1()
}
只是這樣就很有趣了,當你有大量會有 overload resolution ambiguity 的 method 或 parameter 在 context receivers 中時,你會需要標註大量的 labeled 了,這樣好像沒省事到哪去。
但事實上會有這種情況,就不合乎 context receivers 的精神了, Context Receivers 的精神是,在該物件底下的 field 的都可以明確被辨別時,才會用這個方法去節省讀寫標記這些物件的力氣,如果有大量需要標記 labeled 的物件,那使用 Context Receivers 就不是那麼的合適了。
當然也有很多方法可以轉圜,只是適合與否得由撰寫者自己決定了,例如官方影片提到的包一層 interface ,或者是乾脆另外寫一個 method ,像是這樣:
context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
methodForA()
methodForB()
}
context(A)
fun methodForA(){
method1()
}context(B)
fun methodForB(){
method1()
}
都是一個轉圜的方式,就看怎麼樣去做取捨了。
Context Receivers 和 Extension 的差別
問題是在, Context Receivers 和 Extension 實作上的差別好像就在 Receiver 的數量,當 Receiver 都只有一個的時候,引擎蓋底下的實作方法甚至相同。
但是在語境上,卻有 in 跟 on 的區別,意思是,在 Extensions 語意上,你是在這個 object 上呼叫這個 method ;在 Context Receivers 語意上,你是在有這個 object 時呼叫這個 method。
所以不會看到 Context Receivers 被這樣寫:
不只 function
如果必要的話,也是可以把 Context Receivers 放進去 class 裏頭的:
class SimpleClass{
context(A, B, C)
fun method() {
println("I want to combine this three parameter $a $b $c")
}
}
fun main() {
val a: A = AImpl()
val b: B = BImpl()
val c: C = CImpl()
val simpleClass = SimpleClass()
with(a) {
with(b) {
with(c) {
sample.method()
}
}
}
}
當然,如果必要的話,也是可以把 Context Receivers 放到 class 上頭:
context(A, B, C)
class SimpleClass {
fun method() {
println("I want to combine this three parameter $a $b $c")
}
}
fun main() {
val a: A = AImpl()
val b: B = BImpl()
val c: C = CImpl()
val simpleClass = with(a) {
with(b) {
with(c) {
SimpleClass()
}
}
}
simpleClass.method()
}
結語
總而言之,我認為 Context Receivers 主要的目標是要區別 Extension 在使用上會有「讓醬油去做飯」這種語法謬誤的情況,若是他在語境上會更接近「在有醬油的情況做飯」。
此外我認為 Context Receivers 也可以解決一些寫語法很攏的問題,就像我們寫 Android 在定義一些 View 的 attribution 時,可能會使用語法糖 apply 或者 run 之類的方法,來省去讀寫一大堆東西的功夫。
雖然這玩意目前還在實驗階段,但還挺讓人期待他未來會怎麼樣被應用的就是了。
如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我寫文章最大的動力。
參考資料
https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md