mtkw.dev

KotlinのReceiver付き関数について勉強する

これは何?

ComposeでColumnScopeや、FlowのFlowCollectorなど、理解するにはReceiver付き関数は避けて通れません。またレシーバー付き関数を覚えると実装の幅も増えるため勉強してきます。

例

class Person {
    fun hello() {
        println("Hello!")
    }
}
 
fun run(
    // 引数でレシーバー関数を受け取る. Personの部分はレシーバー型と呼ぶ.
    block: Person.() -> Unit
) {
    val person = Person()
    // レシーバー関数を実行するpersonインスタンスをレシーバーと呼ぶ
    person.block() // レシーバー関数を実行する
}
 
fun main() {
    run {
        // レシーバー関数のthisはPersonになるため、hello関数を呼ぶことができる.
        hello()
    }
}

このように書けます。 この言語使用のおかげでKotlinでDSLっぽいAPIを作ることができます。

html {
    header {
        title("my web site")
    }
    body {
        text("Hello")
    }
}

例えばこのようなHTMLを生成するビルダーがあると直感的です。

class HTMLBuilder {
    val headerBuilder = HeaderBuilder()
    val bodyBuilder = BodyBuilder()
    class BodyBuilder {
        var body: String? = null
        fun text(text: String) {
            body = text
        }
    }
 
    class HeaderBuilder {
        var title: String? = null
        fun title(text: String) {
            title = text
        }
    }
 
    fun header(block: HeaderBuilder.() -> Unit) {
        headerBuilder.block()
    }
 
    fun body(block: BodyBuilder.() -> Unit) {
        bodyBuilder.block()
    }
}
fun html(block: HTMLBuilder.() -> Unit) {
    val builder = HTMLBuilder()
    builder.block()
    println(
        """
            <html>
                <header>
                    <title>${builder.headerBuilder.title}</title>
                </header>
                <body>
                    <p>${builder.bodyBuilder.body}</p>
                </body>
            </html>
        """.trimIndent()
    )
}

何も汎用的ではありませんが、レシーバー関数を利用してコンパイルが通るコードができました。
『高階関数+レシーバー付き関数+ラムダ』で綺麗なDLSが書けることがわかりました。

Cold Flowの実装を覗く👁️

val f1 = flow {
    emit("1")
    emit("2")
}
 
scope.launch {
    f1.collect { value -> 
        println(value)
    }
}

上記のコードでコールドフローを作成することができます。
作成といえばBuilder関数なのでこのケースにおいてもレシーバー関数がうまく利用されています。

まず、flow{}の第一引数はsuspend FlowCollector<T>.() -> Unitであり、FlowCollectorをレシーバーとするsuspendレシーバーラムダです。
flow{}ではSafeFlowというインスタンスを作成しているのですが、このインスタンスにラムダを保持します。flow{}が行っているのはそれだけです。

次に、collect{}の第一引数はFlowCollector<T>であり、flow{}の第一引数で渡したレシーバー関数のレシーバーとなるものを渡しています。

ここでSAM(Single Abstruct Method)という言語機能があります。単一の関数を持つinterfaceは無名でインスタンス生成が可能です。

public fun interface FlowCollector<in T> {
 
    /**
     * Collects the value emitted by the upstream.
     * This method is not thread-safe and should not be invoked concurrently.
     */
    public suspend fun emit(value: T)
}

こちらがFlowCollectorの定義です。emitという関数を1つだけ持つinterfaceであるためSAMになります。そのため { value -> }でインスタンス生成が可能なわけです。

ここまで読むと処理の流れを理解することができます。

  1. flow{}でSafeFlowを作成
  2. collectで購読開始
    1. collect関数の第一引数に渡したFlowCollectorというレシーバーで、flow {}の第一引数のレシーバー関数を実行
    2. レシーバー関数内でemitが実行される = FlowCollector#emitが実行される = collectの引数の関数が実行される

大まかにこのような流れであることがわかりました。

まとめ

レシーバー付き関数はBuilderパターンにおいて特に便利であり、ComposeやFlowなどでも利用されていることがわかりました。