Skip to content

Instantly share code, notes, and snippets.

@shinmiy
Last active October 25, 2020 04:26
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shinmiy/6c83e616379a0d8f97c204b92f354e96 to your computer and use it in GitHub Desktop.
Save shinmiy/6c83e616379a0d8f97c204b92f354e96 to your computer and use it in GitHub Desktop.
Kotlin 1.4 Online Eventで発表されたトークについてのメモ

A Look Into the Future

Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ

発表動画: A Look Into the Future by Roman Elizarov - YouTube

Kotlinの歴史

  • 2016: Kotlin 1.0 - JVM、Androidリリース
  • 2017: Kotlin 1.1 - JSサポート、Coroutinesなど
  • 2017: Kotlin 1.2 - マルチプラットフォーム
  • 2018: Kotlin 1.3 - Coroutinesの安定化、Kotlin Native
  • Kotlin 1.4 - 新しい機能を追加しつつ、品質と安定化に重きをおいた。

Near Future

  • Sharing code
    • Kotlinの歴史からわかるようにマルチプラットフォームに多大な努力を注いでいる
    • Kotlin Multiplatform Mobileもその一環
      • AndroidとiOSでコードより簡単に共有できるように、品質もあげて
  • Kotlin JVM
    • Kotlinのルーツなので、こちらも重要視
    • 特に今後Javaにこれから入る機能とのinterop
    • 新しいAPIがシームレスに使えるような互換性
    • Records、Sealed Classes

Distant Future

※まだ意見・憶測の段階。ぜひ意見がほしいので伝えている

ネームスペースと拡張

Adding statically accessible members to an existing Java class via extensions : KT-11968

val Intent.Companion.SCHEME_SMS:String get() = "sms"
  • YouTrackの一番人気: サードパーティ製の型にCompanion作って拡張させたい
  • Companionを拡張しようとするとCompanionのインスタンスをレシーバーとして渡さないといけないが、サードパーティ製のライブラリだと違うひとが拡張すると違うインスタンスを渡してしまうことになる

このように実装の仕方が不明呂なアイディアが挙がったときは、「何を達成しようとしているのか」「なぜこれが欲しいのか」を考えるようにしている(この場合は Intent.SCHEME_SMSと書きたい)

「なぜ」が分かったところで似た課題から探す:

標準ライブラリの実装にも、 Delegates.notNull()と書かせたいからobjectを使って実装しているところがある

Object Delegates {
    fun <T : Any> notNull(): ...
    //
}

Kotlinではobjectの用途は3つある

  • インスタンス: val x = Delegates
  • 型:x is Delegates
  • ネームスペース:Delegates.notNull()

Delegatesではネームスペースとしてしか使っていないので、ほかはただの負債(↑の例でも同じ)

  • ネームスペースのみを指定できる方法があったとしたら?
    • インスタンスも型も作らない namespaceキーワードがあったとしたら?
    • もう少し掘り下げてcompanion objectでもnamespaceが使えたら?
  • 実は古くからある問題も一緒に解決できて、KotlinのクラスをよりJVMに近い形でコンパイルするようにできる
  • namespaceを拡張する選択肢も考えられる
    • val namespace<Intent>.SCHEME_SMS: String get() = "sms"
    • 右側のコードもレシーバーのことを考えなくてよくなる
    • 結果Intent.SCHEME_SMSと書けるようになる

Multiple receivers

Multiple receivers on extension functions/properties : KT-10468

  • YouTrackでもKEEPでもよく挙がる課題
  • Kotlinにはmember extensionsがある
Class View {
    Fun Float.dp() = this * resources.displayMetrics.density
}
  • FloatとViewの2つのレシーバーがある。
  • クラスの中には書けるが、クラスの拡張としては書けない
    • fun (View, Float).dp() = this * resources.displayMetrics.density
    • fun View.Float.dp() = ...
    • fun Float.dp(implicit view: View) = ...
    • どの例も「Kotlinっぽく」はない…(他の言語から取ってたり、思いつきだったり)
  • Kotlinに相応しい構文にするには?
    • Syntactic analogy

with構文が近い

with (view) {
    42f.dp()
}
//
with<View>
fun Float.dp() = this * resources.displayMetrics.density

さらに発展させてinline functionを活用してみる

inline fun <T> withTransaction(block: () -> T): T {
    val tx = beginTransaction()
    return try {
        block()
    } finally {
        tx.commit()
    }
}
// とinline functionを書いたら↓こうやって使う
fun doSomething() {
    withTransaction {
        // code
    }
}
  • inline functionのメリット
    • 「魔法」がないこと。
      • 関数が呼ばれていることは誰が見ても明らか。
      • 対してアノテーションで実装すると、実装が不透明になる
  • デメリット
    • 階層が増える(しかも増えれば増えるほどコードが右にずれていく)
  • 新しく「decorator」キーワードを追加するのはどうだろう?
inline decorator fun <T> withTransaction(block: () -> T): T {
    val tx = beginTransaction()
    return try {
        block()
    } finally {
        tx.commit()
    }
}

@withTransaction
fun doSomething() {
    // code
}
  • inlineとアノテーションの両方の利点を享受できる
    • 簡潔な構文
    • 完全な透明性(クリックしたら実装に飛べる)
  • 他の言語でも似た仕様はあるが、Kotlinでは静的に実装できる
    • コンパイル時にinline化できる

… これがどうmultiple receiverの問題の解決になるのだろうか?

  • 外部のコンテキストが必要なパターン
    • inline decorator fun <T> Tx.withTransaction
    • 装飾されるコードもレシーバーを必要とする
inline decorator fun <T> Tx.withTransaction(block: () -> T): T {
    val tx = beginTransaction()
    return try {
        block()
    } finally {
        tx.commit()
    }
}

@withTransaction
fun doSomething() {
    // Txを受け取る
    // code
}

このアイディアを使うと、新しく特殊な構文を導入しなくてもKotlinで可能な強力なメタプログラミングの一部として存在できる

@with<View>
fun Float.dp() = this * resources.displayMetrics.density

Public/private property types

ただ全部が全部↑のような壮大な話にすることはない

Support having a "public" and a "private" type for the same property : KT-14663

private val items = mutableListOf<Item>()
    public get(): List<Item>
  • 同じプロパティに対してpublicとprivateで別々のタイプを設定できるようにしたい
  • わかりやすいし、_を使ったプロパティ名のボイラープレートをなくせる
// 慣例的に「_」を使った命名
private val _items = mutableListOf<Item>()
val item : List<Item> by _items

比較的シンプルな機能の提案の例。エッジケース、コンパイル時の問題、他の機能との整合性など、一応こういった機能でも何らかのデザインは必要

三項演算子

Support ternary conditional operator 'foo ? a : b' : KT-5823

JavaやCなどで使われる三項演算子が使えるようにしたい

foo ? a : b

シンプルな提案だが問題がいくつかある

  • コードスタイル問題
    • ただKotlinにはすでにifがある
      • if (foo) a else b
      • 既存のコードはどちらにするべきだろう?
  • 初学者には難しい、構文の一貫性
    • Kotlinにおいて?はnullablilityの文脈で使われている
      • Kotlinで?がでてくる=nullabilityに関連している
      • foo ?: bはnullチェック
      • 構文に一貫性がなくなってKotlin初学者がコードを読み解くのが難しくなる(JavaやC/C++を知らなければなおさら)
  • 現実ではあまり出番がない
    • fooなど短い構文で真価を発揮する
    • KotlinではタイプセーフなAPIを提供するようにしている
      • Booleanよりもタイプセーフなenumやリッチなタイプの使用を推奨している
      • 仮にあったとしても長い名前で真価が発揮されにくい

ということで却下。

  • ただこういった書き方に対する需要があるということは感知することができた
  • 将来的になにか別の案として実装されるかもしれない

Immutability

YouTrackのissue以外でも新機能の案がでたりする。世の中の大きなトレンドなど…で、そのひとつがImmutability

例えば:

data class State(
    var lastUpdate: Instant,
    var tags: List<String>
)

state.lastUpdate = now()
state.tags += tag
  • mutableなクラスは宣言も更新も簡単
  • 問題もある
    • モダンなアプリでは、アプリ内外で非同期なデータのやりとりが発生する
    • 非同期なパイプラインを通じて状態を渡す場合、途中変化してしまうのでmutableでは単に送ることができない。
    • 「防御的なコピー」を行うことで今は防いでいる
      • notifyOnChange(state.copy())
      • やることを忘れてしまいやすく、エラーの根源となったりする
  • valをつかってイミュータブルにして解決する方法
    • val lastUpdate: Instant
    • ただしこの方法だと、デメリットが多い
      • 更新しづらい(copy)
      • +=といったオペレーターが使えなくなる
    • →KotlinにおいてImmutableなデータは2nd-class citizenな印象を与えている
  • mutableなクラスのメリットを保ちつつimmutableなクラスの安全性を確保できないだろうか?

Value-based class

val class State(
    val lastUpdate: Instant,
    val tags: List<String>
)
  • valueが状態を定義するクラス
  • 自分のidentityをdisavow(否定)するクラス
  • よくよく考えてみるとKotlinの標準ライブラリにおいては、ほとんどのクラスがvalue-based
    • Int、Longなどはコンパイラのチェックも入る(identityをもとに比較はできない)
    • identityはあるが、一時的なもので変化する

安定したidentityがないことと定義することで、syntactic sugarを追加することができる

val class State(
    val lastUpdate: Instant,
    val tags: List<String>
)

state.lastUpdate = now()
state.tags += tag

notifyOnChange(state)

このアイディアはKotlinの発展の方向として面白いが、短期的な問題も解決できそう。

Inline classes問題

inline class Color(val rgb: Int)
  • inlineモディファイアを使って記述しているが、JavaのProject Valhallaがでると問題になる。
  • Javaの機能との互換性はKotlinにとって重要なので、Valhallaがでた時にKotlinはどうやって書けるようになってるべきかを考えている
  • Java inline classesは、Kotlinのinlineと近いけどコンセプトがかなり違う
    • 混乱を招いてしまうので、何か違う名前を考えたい
  • Value-based classes!
inline class Color(val rgb: Int)
//
@__TBD__
val class Color(val rgb: Int)
  • inline classモディファイアをつけずに、単にvalue-based classとして宣言すればいい
    • 安定したidentityはないので、定義としてもあってる
    • 何かしらのアノテーション(@__TBD__)をつけて、必要ないときはboxしないようにしてすることもできそう
      • アノテーションの命名はJavaとの衝突を避けるためにできるだけ遅らせたい
        • Valhallaが出たときに@JVMInlineといった命名の可能性も残しておける

Kotlinコンパイラとプラグイン

  • ここでは個々の問題を見てきたが、各方面すべての希望をかなえられないしあまりスケーラブルではない
  • ここ数年Kotlinコンパイラ全体を再設計していて、プラグインに対応させている
    • フロントエンド側
      • コンパイラはフロントエンドでソースを解析→フロントエンドでプラグインを書くと新たな構文を定義できる
    • バックエンド側
      • バックエンドの統一してそこからJVM、JS、LLVMのコードを生成するように進めている
      • こうすることでバックエンド側でプラグインを書くとすべてのプラットフォームに適用できるようになる
    • 新しい言語機能をハードコードせずにプラグインとして導入できるようになる
  • 例1: Jetpack Compose (Google)
    • @Composableアノテーションはれっきとした言語機能で、suspending functionと似ている
    • suspending functionとの違いは、Kotlinコンパイラにハードコードされずにプラグインとして存在している
  • 例2: 微分可能プログラミング (Facebook)
    • @Differentiableアノテーション
    • これもプラグインとして実装されている言語機能
  • 他にもArrow KTPower assertsライブラリ
  • 現段階ではexperimentalだが将来的にstableになる

まとめ

  • 話したこと
    • JVM互換性へのコミットメント
    • Kotlinにおけるネームスペースと拡張、複数receiverとdecorators
    • pulbic/privateプロパティタイプ
    • 三項演算子
    • イミュータビリティとinlineクラス
    • Kotlinへのコントリビューション
  • 話しきれなかったこと
    • 代数タイプの構文をより簡潔にできないか
    • collections、tuplesといったデータリテラルを一貫性のある形でKotlinに導入できるか
    • プロパティの構文をフレキシブルに
    • ライブラリ作者に、メンテナンスのしやすい、発展させやすい、より表現力の高いAPIを提供できないか
  • コミュニティのみなさんをみている
    • なにに興味をもっているか
    • どんな問題に直面しているか
    • どんなユースケースがあるか
    • 何が欲しいかを伝えて欲しい

Have a nice Kotlin!

Coroutines Update

Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ

発表動画: Coroutines Update by Vsevolod Tolstopyatov - YouTube

Coroutines debugging

  • 今までずっとつらい部分だった
    • 不完全なスタックトレース
    • ローカル変数がない
    • サスペンドされているコルーチンを見つけるのは至難の業

たとえば、こんなコードがあったとして:

private suspend fun processUserEvents() {
    while (someCondition) {
        val element = channel.receive()
        processElement(element)
    }
    workDone()
}
  • ここにバグがあったとして、どう対処するか。
    • ユニットテストかサンプルアプリを書いて…
    • break point入れてみて…
    • 実行してみて…
    • デバッガーを見てみて…
      • Stack frameはでる
      • けど見れるのはprocessUserEventsのローカル変数と引数くらい
      • 呼び出しもとなどは見ることができない
  • いろいろ見えないのは、バグではない。
    • コルーチンの内部実装によるもの
    • けど、もっとみたいよね…

IntelliJ IDEA 2020.1

ちゃんとしたスタックトレースがみえるようになった!

  • 何が変わった?
    • プログラムの内容、Kotlinコンパイラなどは変わってない
    • IDEAが賢くなった
      • スレッドがコルーチンを実行していることを検知してスタックトレースを表示
      • スタックトレース自体は本物ではないが、本物と同じことができる
        • スタックフレームが見れたり、ローカル変数や引数を覗けたり、コードを実行できたり
  • kotlinx.coroutines 1.3.8+ではCoroutinesタブが表示される
    • アプリが実行しているコルーチンがすべて見える
      • 実行中のコルーチン
      • サスペンドされているコルーチン
      • 作成後まだ実行されてないコルーチン
    • Threadsタブと同じように状態やローカル変数などが確認できる
    • コルーチンの生成スタックトレースもあるので、作成もとが謎のコルーチンが発生しても追える

IntelliJ IDEAとデバッガーは、コルーチンをFirst-class supportしている。これらの変更でコルーチンのデバッグも通常のでデバッグ並みに快適になるはず。

Have a nice Debugging!

Flow

1.3からのおさらい

Flowとはシーケンス

val flow: Flow<Int> = flow {
    delay(100)
    for (i in 1..10) {
        emit(i)
    }
}.map {
    delay(100)
    it * it
}

Kotlin自体のシーケンスと同じように、作って、変化させたりマップしたりフィルターしたり…ができる。

通常のシーケンスとの違いはFlowの中ではビルド中だろうがマップ中だろうが、どこでもサスペンドできること。バックプレッシャーの管理としても機能する。

つまり、FlowはKotlinのシーケンスと同じくらい手軽で、リアクティブプログラミングの恩恵も受けられる。

「状態」の問題

State A condition or way of being that exists at a particular time

ー Oxford English Dictionary

(Stateの定義…特定の時間に存在する、ものの状態あるいはあり方)

たとえばInt:

var variable: Int = 42
  • 作成から破棄までにたくさんの値を持つことができる
  • ひとつの瞬間においてはひとつの値しか持たない
  • 使う側は最新の値にしか興味がない

たとえばダウンロード状態:

  • Not Initialized
  • Started
  • In Progress
  • Successful / Failed

使う側は最新の値にしか興味がない(ダウンロードが終わっていたらIn Progressだった値は不必要)

KotlinではStateをどう管理するか。 以前まではConflatedBroadcastChannelを推奨していたが、Channelなので複雑だった。(ライフサイクルがあったり、APIがたくさんあって、状態管理の用途に対しては重い)

StateFlow

StateFlow - kotlinx-coroutines-core

StateFlowは値と状態をもつただのFlow。他のAPIと同じように2つの種類を提供している:

public interface StateFlow<out T> : Flow<T> {
    public val value: T
}

public interface MutableStateFlow<T> : Flow<T> {
    public override var value: T
}

外部に公開する部分と内部的に隠蔽する部分を自由に決められる。valueを更新することですべてのflow collectorsに反映される。

これを使ってダウンロード状態を管理すると?

class DownloadingModel {
    // 内部向けのMutableStateFlow
    private val _state = MutableStateFlow<DownloadStatus>(DownloadStatus.NOT_REQUESTED)
    // 外部向けにStateFLowを公開する
    val state: StateFlow<DownloadStatus> get() = _state
    
    suspend fun download() {
        // MutableStateFlowを初期状態に
        _state.value = DownloadStatus.INITIALIZED
        initializeConnection()

        // ダウンロード中…
        processAvailableContent { partialData: ByteArray,
                                  downloadedBytes: Long,
                                  totalBytes: Long ->
            storePartialData(partialData)
            // プログレスを更新
            _state.value = DownloadProgress(downloadedBytes.toDouble() / totalBytes)
        }

        // 完了したので状態を更新
        _state.value = Download.SUCCESS
    }
}
  • No channels
  • No coroutines
  • No new concepts
  • Simple!

イベントストリームを管理したい場合は?

イベントストリームの例

例: CO2モニター

  • CO2モニター
    • 「イベント」(=CO2濃度の値)のソース
    • 最新の数件しか必要ない(最新の値・数件で平均をとったり)
  • event processing systemsの特徴
    • 接続コストが高い
      • 接続・切断に数秒かかったりするので、リスナーを共有しておきたい
    • Lazy
      • 接続して値を取得する前にアプリが終了される場合がある
      • 取得がキャンセルされる場合がある
      • 必要になった時にはじめて接続したい(Cold)
    • Replay log
      • クライアント側は最新の数件の値しか必要としない
      • 新しいイベントは来ないかもしれない
        • ハードウェアの故障など
      • ※Channelでは解決できない問題
    • Flexibility
      • 構成によって切断するタイミングが違う
        • すぐに切断するパターン
        • 数秒待ってから切断したいパターン(待ってる間の他で接続したくなるなど)
      • Replay logをしばらくキャッシュ vs すぐに破棄

既存の解決方法

Project ReactorやRxJavaのチームが素晴らしい働きをしてくれている

  • 新しいコンセプトを登場させた
    • Subjects
    • ConnectableFlowable
    • Processors
    • ドメイン固有のオペーレーター
      • share、replay、refCount、connect、autoConnect
  • 自分で解決方法を用意する際にはすごく参考になる(というか後述のSharedFlowでも参考にした)

ただしJavaベースなので、CoroutinesやFlow、バックプレッシャーの管理などKotlinにそのまま転用できないものが多い

SharedFlow

SharedFlow - kotlinx-coroutines-core

replayCacheをもつただのFlowで、atomic snapshotとして読める。これも2種類用意した

interface SharedFlow<out T> : Flow<T> {
    public val replayCache: List<T>
}

interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
    suspend fun emit(value: T)
    fun tryEmit(value: T): Boolean
    val subscriptionCount: StateFlow<Int>
    fun resetReplayCache()
}

MutableSharedFlowのほうが少しトリッキー

  • サスペンドされたcontextとそうでないcontext両方から値を更新できる
  • replay cacheをリセットできる
  • collector countをFlowとして公開
    • フレキシブルにするため
      • 購読カウントが0になったら数秒待ってから切断する、など

便利メソッドも用意した:

public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

完全にカスタマイズ可能

すでにFlowを使っていて、Shareにしたいがコードを書き直すのが難しい場合:

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    replay: Int,
    started: SharingStarted = SharingStarted.Eagerly
)

実例は時間の関係で紹介できないが、ドキュメントやガイドにたくさんある

Flow since 1.3

他にもいくつかアップデートがある

Core operators

  • catch, onEmpty, onCompletion, onStart
  • onEach, transform, transformWhile

Flowで任意の変換が行えるようになる、構成要素となるようなオペレーター。

  • たくさんのフィードバックやリクエストを受けて検討した結果
  • 皆さんのユースケースはどれも妥当だったが、全部含めるとものすごい数になってしまう
    • hard to name(命名が大変)
    • hard to learn(学習が大変)
    • hard to maintain(メンテナンスが大変)
    • hard to keep consistent(それぞれの整合性を保つのが大変)
  • 低レベルな要素を組み合わせて必要な変換を行えるようにした

Enforced Flow invariants

  • contextの保護
  • exceptionの透明性

を担保する。Flowオペレーターを安全性、他のオペレーターの干渉を防ぐのに重要

概念的な説明になるので、実際の例をみてみる:

こんなオペレーターがあった場合に

suspend fun Flow<Int>.stopOn42() = collect {
    println(it)
    if (it == 42) {
        throw AnswerFoundException()
    }
}

こんなFlowで使うとどうなるのか

flow {
    try {
        emit(42)
    } catch (e: AnswerFoundExeption) {
        emit(21)
    }
}.stopOn42()
  • そもそもの話で、例外の透明性を阻害している
    • 下流でもう以降の値に興味がないということで例外を出していているのに、値を発行しようとしている
    • first, firstOrNullなど既存のオペレーターが壊れる可能性がある
    • 初期リリースのFlowでは、42も21の発行されてしまう

新しいkotlinx.coroutinesでは例外が発生する:

java.lang.IllegalStateException: Flow exception transparency is violated
Previous 'emit' call has thrown exception
java.util.concurrentCancellationException: Thanks, I had enough of your data, but then emission attempt of '21' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
at
kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:114)

例外の説明通り、Flow.catchを使ってみる:

flowOf(42)
    .catch { e -> println("Answer was found") }
    .stopOn42()

明確で簡潔なFlowのできあがり。

Android Update

コルーチンを使ったアプリは最適化されてDEXサイズが30%減った

  • いくかのActivityを含む簡単なアプリを書いて計測
  • Flow、channel、coroutine scope、coroutine、launch、asyncを含めた
  • R8とminifyを使ってビルド
  • 結果、30%改善したことがわかった
    • さらに副作用としてkotlinx.coroutinesを使ったアプリの起動時間も減少
  • 最適化ポイント
    • クラスファイルの減少
    • service loaderを完全に書き直してR8、ディスクにやさしくした

dispatchers.defaultdispatchers.ioに使用されるcoroutines schedulerの書き直し

JDK update

withTimeout(500.milliseconds) {
    runInterruptible(Dispatchers.IO) {
        blockingQueue.take()
    }
}

runInterruptibleが新しく導入された

  • blocking-interruptibleなJavaの世界とnon-blocking un-cancellableなKotlinの世界をつなぐ。
  • cancellationとthread interruptionを相互に変換する
  • 外側のcancellationsも一緒に変換
    • withTimeout coroutineがキャンセルされたり、その外側がまるごとキャンセルされると
      • スレットが中断され
      • blockingQueue.take()はinterruption exceptionをスローし
      • 関数がcancellation exceptionに変わる
  • runInterruptible関数で包むことでコルーチンがJavaコールに邪魔されることがなくなる

その他にも…

Future of coroutines

SharedFlow and StateFlow

  • SharedFlowとStateFlowの安定化が最優先
  • 使って、壊して、フィードバックがほしい

Resource management

  • channel越しにやり取りするclosableなリソースがあって、両側からキャンセルされる可能性がある場合はどんな実装になる?
  • 今は難しいが1.4で新しいAPIを導入して解決できるかも

Replacement for offer and poll

  • Channelのoffer/pollメソッドに対するフィードバックをもらっている
  • サードパーティ製の個ーづバック付きAPIで非常に便利だが、大きな落とし穴がある
    • Boolean/nullable型、false/nullを返すはずが、Channelが閉じられていると例外が発生する
    • コードレビューで見つけづらい
  • これらのAPIを非推奨にして、新しいAPIを実装する
    • inlineクラスやsealedクラスを使って、戻り値に例外の型を反映させる

Flow API

  • debounce, sample, throttleといった時間に関連するオペレーターを追加予定
  • UIなどで活用できそう

Coroutine testing

  • kotlinx-coroutines-test のテストUXはまだ改善の余地あり
  • バグフィックス、安定化予定
  • もしかしたらマルチプラットフォーム化してcommon codeで使えるようにも。まだ未定

Sliceable dispatchers

  • sliceable dispatchersを実装する
  • dispatcher.defaultなどの既存のdispatcherを「スライス化」してアプリの並行性を制限
  • 新しいスレッドは作成せず、既存のスレッドを活用する
    • アプリの使うスレッド数を節約して、結果的にバッテリーの持ちをよくする

Have a nice Kotlin!

kotlinx.serialization 1.0 by Leonid Startsev

Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ

発表動画: kotlinx.serialization 1.0 by Leonid Startsev - YouTube

kotlinx.serialization

jacksonやmoshiやgsonがあるのに、またJSONパーサー?

Multiplatform

KotlinJVM、KotlinJS、Kotlin Nativeをサポートするマルチプラットフォームライブラリ

Kotlin-oriented

こんなdata classがあったとして

@Serializable
data class Project(
    val name: String,
    val language: String = "Kotlin"
)

const val inputString = """{"name":"kotlinx.serialization"}"""

println(Json.decodeFromString<Project>(inputString))
// Project(name=kotlinx.serialization, language=Kotlin)

Gsonでパースしようとすると、language要素がないのでランタイムエラーで落ちる。kotlinx.serializationはデフォルト値を読めるので、language=Kotlinが入る

Explicit & compile-time safe

@Serializableアノテーションがついていないクラスを含めようとすると、コンパイル時にエラーが起きる

// not @Serializable
data class User(val userName: String)

@Serializable
data class Project(
    val name: String,
    val owner: User, // error: Serializer for type User has not been found.
    val language: String = "Kotlin"
)

Explicit & concise

明確 != 冗長

ProjectクラスのListを使った例:

// Gson
val projectsList = Gson().fromJson<List<Project>>(
    inputStringList,
    List::class.java
)
println(projectsList.first()::class.java)
// class com.google.gson.internal.LinkedTreeMap

// Gson (workaround)
val projectsListWorkaround = Gson().fromJson<List<Project>>(
    inputStringList,
    (object: TypeToken<List<Project>>() {}).type // workaround
)
println(projectsListWorkaround.first()::class.java)
// class kotlinx.serialization.formats.json.Project

// kotlinx.serialization
val projectListKxs = Json.decodeFromString<List<Project>>(inputStringList)
println(projectListKxs.first()::class.java)
// class kotlinx.serialization.formats.json.Project

Gsonではリストを扱う場合はtype tokenを使う必要があるが、匿名オブジェクトを作ったりとかなり冗長。kotlinx.serializationを使うともっと簡潔に書ける。

Kotlin1.4からは新しくKTypeを返すinlineでreifiedなtypeOf関数が登場する。KTypeがtype token的役割を担っていて、複雑な型でも取得できる。

public inline fun <reified T> typeOf(): KType

val type = typeOf<Box<List<StringData>>>()
println(type)
// "kotlinx.serialization.Box<kotlinx.serialization.List<kotlinx.serialization.StringData>>"

しかもKotlinコンパイラそのものの機能なので、匿名オブジェクトを作ったりランタイム時のフットプリントもない。どのプラットフォームでも動く。

これを使って自前のserializer関数を作ることも可能

public inline fun <reified T> serializer(): KSerializer<T>
val serial = serializer<Box<List<StringData>>>()
// or
val serial = Box.serializer(StringData.serializer().list)

val box = Json.decodeFromString(serial, input)

※KSerializerについては、KotlinConf 2019の発表を参照: Design of kotlinx.serialization | KotlinConf 2019 - Kotlin Programming Conference

Many formats with similar APIs

JSONだけじゃなく、CBORやprotocol buffersでも使える。コミュニティ製のフォーマットも足せる。

命名規則も一貫していて、encodeencodeToといった名前がつく(protocol buffersではencodeToByteArray、JSONはencodeToStringなど)。逆も同様(decodeFrom

Future-proof configuration

Future-proofは難しい:Public API challenges in Kotlin - Jake Wharton

1.0未満ではJsonConfigurationというクラスがあってフラグをたくさん持っていたが、機能を追加しようとすると互換性の問題にあたる。

public data class JsonConfiguration(
    // ...
    val coerceInputValues: Boolean = false,
    val coolNewJsonFeature: Boolean = false, // 新しく追加したとして…
    val userArrayPolymorphism: Boolean = false,
    // ...
)

// 一見すると動きそうだが、binary dump見てみるとコンストラクターが変わっていてinit関数がなくなって
// Exception in thread "main" java.lang.NoSuchMethodError: JsonConfiguration.<init>

Configuring is easier

Design choiceとしてDSLで解決した。

Json(JsonConfiguration(ignoreUnknownKeys = true))

//

Json {
    ignoreUnknownKeys = true
}

JsonConfigurationをやめて、DSLブロックでJsonを設定できるようにした。こうすることで機能を追加してもbuilderクラスにゲッターとセッターが増えるだけで、影響を少なくできる。

Copying is eaisier

副次的な効果として、使い回しが楽になった。

val myConfig = JsonConfiguration.Default.copy(encodeDefaults = false)
val myJson = Json(myConfig)
val myLenientJson = Json(myConfig.copy(isLenient = true))

//

val myJson = Json { encodeDefaults = false }
val myLenientJson = Json(myJson) { isLenient = true }

その他の機能やガイドについては、Githubリポジトリを参照。

Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization

1.0 release APIs

今回の1.0リリースでは、2つのフレーバーができた

  • Stable
  • Experimental
    • 皆さんのフィードバックがほしい

Stableの機能

  • Stringからのシリアライズ、Stringへのデシリアライズ
  • @Serializableとシリアライズ関連アノテーション
  • ポリモーフィズムへの対応
  • JSON tree API (custom serialization含む)
  • フォーマットに依存しないcustom serializers

@Serializableクラスのシリアライズ・デシリアライズはstableで、binaryレベルで後方互換性を持つ。以後新しいcompiler pluginは1.0まで互換性を持たせる。(=kotlinx.serializationをアップデートしたくなければしなくてもいい)

Experimental

  • Custom serial formats
    • Experimentalのほうでは自前のserial formatを用意することができる
    • まだExperimentalなのは、クラスについてなどフィードバックがほしいから
  • Schema introspection
    • limited reflection capabilities for serializable classes
  • CBOR, Protobuf, HOCON, and Properties
    • 機能が足りなかったり、バグがあったりするのでまだexperimental
    • バグを見つけて起票してほしい
  • @ExperimentalSerializationApi
    • ↑以外でもExperimentalな機能にはアノテーションがつく

これらの機能は将来的には変更になる場合がある。そうなった場合はマイグレーションを補助する方法も可能な限り提供する。ぜひ使ってもらって、フィードバックがほしい。

Internal functionality

kotlinx.serialization.internal.*@InternalSerializationApiアノテーションがついてる内部APIがいくつかある。次回以降のリリースで削除される可能性があるが、使用していたりひつようとしているのであれば、public APIとして提供できるようにGitHub issuesなどで教えて欲しい

1.0 features

Changelogに一覧があるが、ここでは2つを紹介

kotlinx.serialization/CHANGELOG.md at v1.0.0 · Kotlin/kotlinx.serialization

More flexible desieralization

null値を値なしとして扱えるcoerceInputValuesフラグが追加された。

val json = Json { coerceInputValues = true }

@Serializable
data class Project(
    val name: String,
    val language: String = "Kotlin"
)

println(json.decodeFromString<Project>(
    """{"name":"Ktor","language":null}"""
))
// Project(name=Ktor, language=Kotlin)

Polymorphic desierialization

kotlinx.serializationはポリモーフィズムに対応している

@Serializable
sealed class Project(
    abstract val name: String,
    abstract val language: String
)

@Serializable
class OwnedProject(
    override val name: String,
    override val language: String,
    val owner: String
): Project
Json.decodeFromString<Project>(
    """{"type":"Owned","name":"Kotlin","language":"Kotlin","owner":"JetBrains"}"""
)
// => OwnedProject(name=Kotlin, language=Kotlin, owner=JetBrains)

@Serializable
class StarredProject(
    override val name: String,
    override val language: String,
    val stars: Int
): Project
Json.decodeFromString<Project>(
    """{"type":"Starred","name":"Kotlin","language":"Kotlin","stars":2200}"""
)
// => StarredProject(name=Kotlin, language=Kotlin, owner=JetBrains)

discriminator(ここでは"type")をもとにそれぞれのクラスにデシリアライズされる。

定義していないtypeをデシリアライズしようとすると、エラーが返される

Json.decodeFromString<Project>(
    """{"type":"Forked","name":"Kotlin","language":"Kotlin","forks":4100}"""
)
// Polymorphic serializer was not found for class discriminator 'Forked'

特にサードパーティ製APIなど、仕様のコントロールが効かない場合などではこの挙動は困るので、そういった場合への対応としてフレキシブルにデシリアライズできる機能が追加された

val responseModule = SerializersModule {
    polymorphic(Project::class) {
        default { className ->
            if (className != null) DefaultProject.serializer()
            else null
        }
    }
}
// => DefaultProject(name=Kotlin, language=Kotlin)

ここでは一律DefaultProjectを返しているが、classNameを受けるdefaultブロックを実装することでdiscrimitatorに応じたクラスを返すことができる

Future plans

IO streaming

要望の多い機能。次のリリースでjava.io streamsに対応するJVM限定のAPIを提供予定

kotlinx-ioについてはkotlinx-ioの安定版がまだ出ていないので、それ次第

inline classes

重要な機能なので忘れてないよ!WIP。

How to set up the library

まだ使ったことのないユーザーに向けてのセットアップガイド

Step1 - Compiler plugin

まずはcompiler plugin。リフレクションを使わない代わりにcompiler pluginを使っている

// Kotlin DSL
plugins {
    kotlin("jvm") version "1.4.0"
    kotlin("plugin.serialization") version "1.4.0"
}

// Groovy DSL
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.4.0'
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.0'
}

Step2 - Runtime library

repositories {
    jcenter()
}

dependencies {
    implementation(
        "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0"
    )
}

安定版はMaven Centralに公開しているが、開発版を利用したい場合はjcenterを追加すること。

kotlinx-serialization-coreはJVMのみでもmultiplatformでもつかえるので、kotlinx-serialization-core-jvmみたいなことは書かなくてもよい

How to migrate

1.0より前のバージョンを使っていたいひとには、マイグレーションガイドを用意している

kotlinx.serialization/migration.md at v1.0.0 · Kotlin/kotlinx.serialization

Step 0 / Step 1

非推奨になっているAPIがいくつかあるが、Kotlin標準の仕組みを使っているのでAlt+Enterで修正していける。

Json.stringify(project)
// Alt + Enter
Json.encodeToString(project)

Step 2

Alt+Enterで解決できない場合は、別のパッケージに移動している可能性がある。その場合は*インポートを追加することで解決する。

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*

最後に

マイグレーションガイドやchangelogをGithubで提供しているので、詳しくはそちらで

Have a nice Kotlin!

New Language Features in Kotlin 1.4

Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ

発表動画: New Language Features in Kotlin 1.4 by Svetlana Isakova - YouTube

Kotlin 1.4

  • 品質とパフォーマンス向上に専念

  • ツールの改良や新しいコンパイラに注力している

  • 新しい型推論アルゴリズムも紹介

  • 新機能

    • KotlinクラスのSAM変換
    • Explicit API mode
    • Trailing commas
    • when文内でのbreakcontinue
    • 名前付き引数とそうでない引数の混在
    • 新しい型推論アルゴリズム
    • nullチェック例外の統一

SAM変換

これまで

interface Action {
    fun run()
}
  • SAM = single abstract method
  • SAM変換 = SAMインターフェースに対してlambda や callable referenceを渡せること
  • Kotlinのリリースから、Javaのインターフェースではできた
// Java
public interface Action {
    void run();
}

public static void runAction(Action action) {
    action.run();
}
// Kotlin
runAction {
    println("I'm Kotlin 1.3")
}
  • Kotlinのインターフェースでやろうとするとコンパイルエラーがでていた。
    • 公式には代わりにパラメーターとして関数型を受けることを推奨していた
  • 5年前からIssueが立っている

Functional Interfaces

  • Kotlin 1.4でfunctional interfacesを導入した
  • fun interfaceキーワードを追加することでSAM変換が可能になる
fun interface Action {
   fun run()
}

fun runAction(a: Action) = a.run()

runAction {
    println("Hello, Kotlin 1.4!")
}

なぜ新しいキーワード?

  • Kotlinでは意図を明確にすることを理念としている
  • SAM変換を念頭にインターフェースを実装したのなら、そう明記するべき
  • たとえば、メソッドが一つのインターフェースを実装して、誰かがSAM変換して使用していた場合、メソッドを追加すると使用先のコードも意図せず壊れてしまう
fun interface Action {
    fun run()
    fun runWithDelay() // 追加した時点でコンパイルが通らなくなる
}

Explicit API mode

  • ライブラリ作者向けの機能としてExplicit API modeを追加した
  • Kotlin スタイルガイドではライブラリ作者に向けた特別なガイドを用意している
  • 推奨1: メンバーの可視性を明記する
    • 誤って意図と違う可視性修飾子をつけることを防ぐ
private fun privateFun() { ... }
public fun publicFun() { ... }
  • 推奨2: 関数の戻り値やプロパティの型を明記する
    • コードの修正によって関数などの戻り値の型が意図せず変わってしまうことを防ぐ
fun getAnswer(finished: Boolean): String = if (finished) "42" else "unknown"
  • Kotlin1.4ではexplicit API modeでこれらのスタイルを強制できるようになる
    • errorかwarningかは選べる
// build.gradle.kts
kotlin {
    explicitApi()
}

// build.gradle
kotlin {
    explicitApi = 'strict'
}
  • ライブラリ作者向けの機能ではあるが、それ以外のプロジェクトにも適用できる

Trailing commas

val colors = listOf(
    "red",
    "green",
    "blue", // <-
  • Kotlin 1.4から最後の行にもカンマを付けられるようになった
    • 関数、クラスの宣言の最後のパラメーターに付けられる
  • 強制ではないので、気持ち悪ければ書かなくてもよい
  • カンマがついた行を入れ替える際はIntelliJ IDEAやAndroid Studioの"Change signature"機能を使ってリファクタリングすることをおすすめする
    • 使用側も自動的に変更される

When文内でのbreakとcontinue

// ~1.3
fun foo (list: List<Int>) {
    l@ for (i in list) {
        when (i) {
            42 -> continue@l // ラベルをつければcontinueが使えた
            else -> println(i)
        }
    }
}

// 1.4
fun foo (list: List<Int>) {
    for (i in list) {
        when (i) {
            42 -> continue // そのまま使えるようになった
            else -> println(i)
        }
    }
}
  • 他の用途がありそうだったので使用を認めていなかったが、外側のループに対してcontinue/breakを使っていることは自明だったのでこの制限を外した

名前付き引数とそうでない引数の混在

// ~1.3
drawRectangle(
    width = 10, height = 20, color = Color.BLUE
)

// 1.4
drawRectangle(
    width = 10, height = 20, Color.BLUE
)
  • 引数の名前の指定・省略が混在できるようになった(=意味が自明な引数は省略できる)
    • 順番が正しい場合のみ

新しい型推論アルゴリズム

  • Kotlinコンパイラを書き直している
    • 一から書き直していて、300以上の問題を解決する
    • functional interfacesに対応
    • ほとんどの場合で型推論が可能
    • より複雑な場面でもスマートキャストが可能
    • callable referencesへの対応範囲が拡大
    • などなど

新しくコンパイルできるようになったコード

例1: lambda parameter type

val rulesMap: Map<String, (String?) -> Boolean> =
    mapOf(
        "weak" to { it != null }, // it が String?なのは自明
        "medium" to { !it.isNullOrBlank() },
        "strong" to { it != null &&
            "^[a-zA-Z0-9]+$".toRegex().matches(it)
        }
    }

例2: ラムダ内の最後の式

// ~1.3 result: String?
// 1.4 result: String
val result = run {
    var str = currentValue()
    if (str == null) {
        str = "test"
    }
    str
}

例3: Callable references

より複雑なシナリオでもcallable referencesが使えるようになった

fun foo(i: Int = 0): String = "$i!"

// 引数としてIntを受けるが、デフォルト値が設定されているので、
// :() -> Stringとして解釈できるようになった
apply(::foo)

nullチェック例外の統一

  • ~1.3: !!、as Typeなどnullチェックはそれぞれ別のExceptionをthrowしていた
    • KotlinNullPointerException
    • TypeCastException
    • IllegalStateException
    • IllegalArgumentException
  • 1.4: NullPointerExceptionに統一される
    • 変わるのはExceptionの型だけで、追加・削除、箇所の変更などはない
  • なぜ変更した?
    • 将来的にKotlinコンパイラや、特にAndroid R8 optimizerの最適化がしやすくなる
    • 複数階層で同じチェックを繰り返し行っている場合の最適化が可能になる
  • 一部のメッセージがなくなってしまうが、最適化とのトレードオフ

インターフェースでデフォルトメソッドを生成する (Experimental)

Kotlinではインターフェースにデフォルトメソッドを定義することができる

interface Alien {
    fun speak() = "Wubba lubba dub dub"
}

class BirdPerson : Alien

本来Java8向け機能なのにJava 6をターゲットしていても動くのは、内部的にはstaticメソッドが生成されるから

public interface Alien {
    String speak();

    // 内部的にはstaticメソッドが生成される
    public static final class DefaultImpls {
        public static String speak(Alien obj) {
            return "Wubba lubba dub dub";
        }
    }
}

public final class BirdPerson implements Alien {
    public String speak() {
        return Alien.DefaultImples.speak(this); // 自動的に挿入される
    }
}

Java 6では問題ないが、Java 8で動かす場合は機能としては無駄になるので、Kotlin 1.2で-Xjvm-default=enableオプションが追加されて、staticメソッドの生成を抑制できるようになった。

interface Alien {
    @JvmDefault
    fun speak() = "Wubba lubba dub dub"
}
  • 2つのモードから選べる
    • -Xjvm-default=enable : デフォルトメソッドのみの生成
    • -Xjvm-default=compatibility : デフォルトメソッドとDefaultImpls両方の生成
  • Kotlin 1.4から、新しく2つのモードが追加されて、これまでの2つは非推奨になった
    • モードの役割は同じだが、@JvmDefaultが必要なくなった
      • -Xjvm-default=all: デフォルトメソッドのみの生成
      • -Xjvm-default=all-compatibility: デフォルトメソッドとDefaultImpls両方の生成
    • 現在はexperimentalだが、将来的にデフォルトで適用される
      • all-compatibilityallの順番
  • @JvmDefaultWithoutCompatibilityアノテーションなど、詳しくはKotlin Blogを参照

最後に

Have a nice Kotlin!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment