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 -> }でインスタンス生成が可能なわけです。
ここまで読むと処理の流れを理解することができます。
- flow{}でSafeFlowを作成
- collectで購読開始
- collect関数の第一引数に渡したFlowCollectorというレシーバーで、flow {}の第一引数のレシーバー関数を実行
- レシーバー関数内でemitが実行される = FlowCollector#emitが実行される = collectの引数の関数が実行される
大まかにこのような流れであることがわかりました。
まとめ
レシーバー付き関数はBuilderパターンにおいて特に便利であり、ComposeやFlowなどでも利用されていることがわかりました。