Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ
-
-
Save shinmiy/6c83e616379a0d8f97c204b92f354e96 to your computer and use it in GitHub Desktop.
Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ
発表動画: A Look Into the Future by Roman Elizarov - YouTube
- 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 - 新しい機能を追加しつつ、品質と安定化に重きをおいた。
- Sharing code
- Kotlinの歴史からわかるようにマルチプラットフォームに多大な努力を注いでいる
- Kotlin Multiplatform Mobileもその一環
- AndroidとiOSでコードより簡単に共有できるように、品質もあげて
- Kotlin JVM
- Kotlinのルーツなので、こちらも重要視
- 特に今後Javaにこれから入る機能とのinterop
- 新しいAPIがシームレスに使えるような互換性
- Records、Sealed Classes
※まだ意見・憶測の段階。ぜひ意見がほしいので伝えている
- どの機能を優先して取り組むのか
- コミュニティをみている
- Slack
- 大規模ユーザーとの直接のコミュニケーション
- YouTrackのissueの起票、投票
- KEEP
- YouTrackとKEEPはどう使い分けてる?
- KEEP: デザインドキュメントとして
- プロトタイプができていて言語に導入してもいいもの
- KEEP issues: それらに対する修正、提案、改良
- YouTrack: 問題、アイディア、提案の場所
language_design
タグで管理- 人気だったissueが解決されているのがわかる
- Kotlin 1.1: Callable reference with expression on the left hand side : KT-6947
- Kotlin 1.2: Allow specifying array annotation attribute single value without arrayOf() : KT-11235
- Kotlin 1.3: Support assignment of "when" subject to a variable : KT-4895
- Kotlin 1.4: SAM for Kotlin classes : KT-7770
- KEEP: デザインドキュメントとして
- コミュニティをみている
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 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
ただ全部が全部↑のような壮大な話にすることはない
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にはすでに
- 初学者には難しい、構文の一貫性
- Kotlinにおいて
?
はnullablilityの文脈で使われている- Kotlinで
?
がでてくる=nullabilityに関連している foo ?: b
はnullチェック- 構文に一貫性がなくなってKotlin初学者がコードを読み解くのが難しくなる(JavaやC/C++を知らなければなおさら)
- Kotlinで
- Kotlinにおいて
- 現実ではあまり出番がない
foo
など短い構文で真価を発揮する- KotlinではタイプセーフなAPIを提供するようにしている
- Booleanよりもタイプセーフなenumやリッチなタイプの使用を推奨している
- 仮にあったとしても長い名前で真価が発揮されにくい
ということで却下。
- ただこういった書き方に対する需要があるということは感知することができた
- 将来的になにか別の案として実装されるかもしれない
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なクラスの安全性を確保できないだろうか?
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 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といった命名の可能性も残しておける
- アノテーションの命名はJavaとの衝突を避けるためにできるだけ遅らせたい
- ここでは個々の問題を見てきたが、各方面すべての希望をかなえられないしあまりスケーラブルではない
- ここ数年Kotlinコンパイラ全体を再設計していて、プラグインに対応させている
- フロントエンド側
- コンパイラはフロントエンドでソースを解析→フロントエンドでプラグインを書くと新たな構文を定義できる
- バックエンド側
- バックエンドの統一してそこからJVM、JS、LLVMのコードを生成するように進めている
- こうすることでバックエンド側でプラグインを書くとすべてのプラットフォームに適用できるようになる
- 新しい言語機能をハードコードせずにプラグインとして導入できるようになる
- フロントエンド側
- 例1: Jetpack Compose (Google)
@Composable
アノテーションはれっきとした言語機能で、suspending functionと似ている- suspending functionとの違いは、Kotlinコンパイラにハードコードされずにプラグインとして存在している
- 例2: 微分可能プログラミング (Facebook)
@Differentiable
アノテーション- これもプラグインとして実装されている言語機能
- 他にもArrow KTやPower assertsライブラリ
- 現段階ではexperimentalだが将来的にstableになる
- 話したこと
- JVM互換性へのコミットメント
- Kotlinにおけるネームスペースと拡張、複数receiverとdecorators
- pulbic/privateプロパティタイプ
- 三項演算子
- イミュータビリティとinlineクラス
- Kotlinへのコントリビューション
- 話しきれなかったこと
- 代数タイプの構文をより簡潔にできないか
- collections、tuplesといったデータリテラルを一貫性のある形でKotlinに導入できるか
- プロパティの構文をフレキシブルに
- ライブラリ作者に、メンテナンスのしやすい、発展させやすい、より表現力の高いAPIを提供できないか
- コミュニティのみなさんをみている
- なにに興味をもっているか
- どんな問題に直面しているか
- どんなユースケースがあるか
- 何が欲しいかを伝えて欲しい
Have a nice Kotlin!
Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ
発表動画: Coroutines Update by Vsevolod Tolstopyatov - YouTube
- 今までずっとつらい部分だった
- 不完全なスタックトレース
- ローカル変数がない
- サスペンドされているコルーチンを見つけるのは至難の業
たとえば、こんなコードがあったとして:
private suspend fun processUserEvents() {
while (someCondition) {
val element = channel.receive()
processElement(element)
}
workDone()
}
- ここにバグがあったとして、どう対処するか。
- ユニットテストかサンプルアプリを書いて…
- break point入れてみて…
- 実行してみて…
- デバッガーを見てみて…
- Stack frameはでる
- けど見れるのはprocessUserEventsのローカル変数と引数くらい
- 呼び出しもとなどは見ることができない
- いろいろ見えないのは、バグではない。
- コルーチンの内部実装によるもの
- けど、もっとみたいよね…
ちゃんとしたスタックトレースがみえるようになった!
- 何が変わった?
- プログラムの内容、Kotlinコンパイラなどは変わってない
- IDEAが賢くなった
- スレッドがコルーチンを実行していることを検知してスタックトレースを表示
- スタックトレース自体は本物ではないが、本物と同じことができる
- スタックフレームが見れたり、ローカル変数や引数を覗けたり、コードを実行できたり
- kotlinx.coroutines 1.3.8+ではCoroutinesタブが表示される
- アプリが実行しているコルーチンがすべて見える
- 実行中のコルーチン
- サスペンドされているコルーチン
- 作成後まだ実行されてないコルーチン
- Threadsタブと同じように状態やローカル変数などが確認できる
- コルーチンの生成スタックトレースもあるので、作成もとが謎のコルーチンが発生しても追える
- アプリが実行しているコルーチンがすべて見える
IntelliJ IDEAとデバッガーは、コルーチンをFirst-class supportしている。これらの変更でコルーチンのデバッグも通常のでデバッグ並みに快適になるはず。
Have a nice Debugging!
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 - 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 - 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
)
実例は時間の関係で紹介できないが、ドキュメントやガイドにたくさんある
他にもいくつかアップデートがある
- catch, onEmpty, onCompletion, onStart
- onEach, transform, transformWhile
Flowで任意の変換が行えるようになる、構成要素となるようなオペレーター。
- たくさんのフィードバックやリクエストを受けて検討した結果
- 皆さんのユースケースはどれも妥当だったが、全部含めるとものすごい数になってしまう
- hard to name(命名が大変)
- hard to learn(学習が大変)
- hard to maintain(メンテナンスが大変)
- hard to keep consistent(それぞれの整合性を保つのが大変)
- 低レベルな要素を組み合わせて必要な変換を行えるようにした
- 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のできあがり。
コルーチンを使ったアプリは最適化されてDEXサイズが30%減った
- いくかのActivityを含む簡単なアプリを書いて計測
- Flow、channel、coroutine scope、coroutine、launch、asyncを含めた
- R8とminifyを使ってビルド
- 結果、30%改善したことがわかった
- さらに副作用としてkotlinx.coroutinesを使ったアプリの起動時間も減少
- 最適化ポイント
- クラスファイルの減少
- service loaderを完全に書き直してR8、ディスクにやさしくした
dispatchers.default
とdispatchers.io
に使用されるcoroutines schedulerの書き直し
- 結果、様々なケースでCPU使用量が大幅に減少
- 詳細についてはPull Requestを参照
- CoroutineScheduler rework by qwwdfsad · Pull Request #1652 · Kotlin/kotlinx.coroutines
- 内部実装の詳細や考慮したトレードオフ、ベンチマークなどが確認できる
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に変わる
- withTimeout coroutineがキャンセルされたり、その外側がまるごとキャンセルされると
- runInterruptible関数で包むことでコルーチンがJavaコールに邪魔されることがなくなる
その他にも…
- BlockHoundへの対応
- Java9のFlowへの対応
- SharedFlowとStateFlowの安定化が最優先
- 使って、壊して、フィードバックがほしい
- channel越しにやり取りするclosableなリソースがあって、両側からキャンセルされる可能性がある場合はどんな実装になる?
- 今は難しいが1.4で新しいAPIを導入して解決できるかも
- Channelのoffer/pollメソッドに対するフィードバックをもらっている
- サードパーティ製の個ーづバック付きAPIで非常に便利だが、大きな落とし穴がある
- Boolean/nullable型、false/nullを返すはずが、Channelが閉じられていると例外が発生する
- コードレビューで見つけづらい
- これらのAPIを非推奨にして、新しいAPIを実装する
- inlineクラスやsealedクラスを使って、戻り値に例外の型を反映させる
- debounce, sample, throttleといった時間に関連するオペレーターを追加予定
- UIなどで活用できそう
kotlinx-coroutines-test
のテストUXはまだ改善の余地あり- バグフィックス、安定化予定
- もしかしたらマルチプラットフォーム化してcommon codeで使えるようにも。まだ未定
- sliceable dispatchersを実装する
dispatcher.default
などの既存のdispatcherを「スライス化」してアプリの並行性を制限- 新しいスレッドは作成せず、既存のスレッドを活用する
- アプリの使うスレッド数を節約して、結果的にバッテリーの持ちをよくする
Have a nice Kotlin!
Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ
発表動画: kotlinx.serialization 1.0 by Leonid Startsev - YouTube
jacksonやmoshiやgsonがあるのに、またJSONパーサー?
KotlinJVM、KotlinJS、Kotlin Nativeをサポートするマルチプラットフォームライブラリ
こんな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
が入る
@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"
)
明確 != 冗長
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
JSONだけじゃなく、CBORやprotocol buffersでも使える。コミュニティ製のフォーマットも足せる。
命名規則も一貫していて、encode
やencodeTo
といった名前がつく(protocol buffersではencodeToByteArray
、JSONはencodeToString
など)。逆も同様(decodeFrom
)
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>
Design choiceとしてDSLで解決した。
Json(JsonConfiguration(ignoreUnknownKeys = true))
// ↓
Json {
ignoreUnknownKeys = true
}
JsonConfigurationをやめて、DSLブロックでJsonを設定できるようにした。こうすることで機能を追加してもbuilderクラスにゲッターとセッターが増えるだけで、影響を少なくできる。
副次的な効果として、使い回しが楽になった。
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リリースでは、2つのフレーバーができた
- Stable
- Experimental
- 皆さんのフィードバックがほしい
- Stringからのシリアライズ、Stringへのデシリアライズ
@Serializable
とシリアライズ関連アノテーション- ポリモーフィズムへの対応
- JSON tree API (custom serialization含む)
- フォーマットに依存しないcustom serializers
@Serializableクラスのシリアライズ・デシリアライズはstableで、binaryレベルで後方互換性を持つ。以後新しいcompiler pluginは1.0まで互換性を持たせる。(=kotlinx.serializationをアップデートしたくなければしなくてもいい)
- Custom serial formats
- Experimentalのほうでは自前のserial formatを用意することができる
- まだExperimentalなのは、クラスについてなどフィードバックがほしいから
- Schema introspection
- limited reflection capabilities for serializable classes
- CBOR, Protobuf, HOCON, and Properties
- 機能が足りなかったり、バグがあったりするのでまだexperimental
- バグを見つけて起票してほしい
- @ExperimentalSerializationApi
- ↑以外でもExperimentalな機能にはアノテーションがつく
これらの機能は将来的には変更になる場合がある。そうなった場合はマイグレーションを補助する方法も可能な限り提供する。ぜひ使ってもらって、フィードバックがほしい。
kotlinx.serialization.internal.*
や@InternalSerializationApi
アノテーションがついてる内部APIがいくつかある。次回以降のリリースで削除される可能性があるが、使用していたりひつようとしているのであれば、public APIとして提供できるようにGitHub issuesなどで教えて欲しい
Changelogに一覧があるが、ここでは2つを紹介
kotlinx.serialization/CHANGELOG.md at v1.0.0 · Kotlin/kotlinx.serialization
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)
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に応じたクラスを返すことができる
要望の多い機能。次のリリースでjava.io streamsに対応するJVM限定のAPIを提供予定
kotlinx-ioについてはkotlinx-ioの安定版がまだ出ていないので、それ次第
重要な機能なので忘れてないよ!WIP。
まだ使ったことのないユーザーに向けてのセットアップガイド
まずは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'
}
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
みたいなことは書かなくてもよい
1.0より前のバージョンを使っていたいひとには、マイグレーションガイドを用意している
kotlinx.serialization/migration.md at v1.0.0 · Kotlin/kotlinx.serialization
非推奨になっているAPIがいくつかあるが、Kotlin標準の仕組みを使っているのでAlt+Enter
で修正していける。
Json.stringify(project)
// Alt + Enter
Json.encodeToString(project)
Alt+Enterで解決できない場合は、別のパッケージに移動している可能性がある。その場合は*
インポートを追加することで解決する。
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
マイグレーションガイドやchangelogをGithubで提供しているので、詳しくはそちらで
- kotlinx.serialization/migration.md at v1.0.0 · Kotlin/kotlinx.serialization
- kotlinx.serialization/CHANGELOG.md at v1.0.0 · Kotlin/kotlinx.serialization
Have a nice Kotlin!
Kotlin 1.4 Online Event, October 12–15, 2020 で発表されたトークについてのメモ
発表動画: New Language Features in Kotlin 1.4 by Svetlana Isakova - YouTube
-
品質とパフォーマンス向上に専念
-
ツールの改良や新しいコンパイラに注力している
-
新しい型推論アルゴリズムも紹介
-
新機能
- KotlinクラスのSAM変換
- Explicit API mode
- Trailing commas
- when文内での
break
とcontinue
- 名前付き引数とそうでない引数の混在
- 新しい型推論アルゴリズム
- nullチェック例外の統一
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が立っている
- 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を追加した
- 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'
}
- ライブラリ作者向けの機能ではあるが、それ以外のプロジェクトにも適用できる
val colors = listOf(
"red",
"green",
"blue", // <-
- Kotlin 1.4から最後の行にもカンマを付けられるようになった
- 関数、クラスの宣言の最後のパラメーターに付けられる
- 強制ではないので、気持ち悪ければ書かなくてもよい
- カンマがついた行を入れ替える際はIntelliJ IDEAやAndroid Studioの"Change signature"機能を使ってリファクタリングすることをおすすめする
- 使用側も自動的に変更される
// ~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)
- ~1.3: !!、as Typeなどnullチェックはそれぞれ別のExceptionをthrowしていた
- KotlinNullPointerException
- TypeCastException
- IllegalStateException
- IllegalArgumentException
- 1.4: NullPointerExceptionに統一される
- 変わるのはExceptionの型だけで、追加・削除、箇所の変更などはない
- なぜ変更した?
- 将来的にKotlinコンパイラや、特にAndroid R8 optimizerの最適化がしやすくなる
- 複数階層で同じチェックを繰り返し行っている場合の最適化が可能になる
- 一部のメッセージがなくなってしまうが、最適化とのトレードオフ
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-compatibility
→all
の順番
- モードの役割は同じだが、
@JvmDefaultWithoutCompatibility
アノテーションなど、詳しくはKotlin Blogを参照
- その他の機能についてもドキュメンテーションページを参照
Have a nice Kotlin!