🕌

個人的に注目しているKotlinプロポーザル5選

に公開

はじめに

Kotlin 2.0でK2コンパイラが本格的に導入され、Kotlin は速くなるだけでなく、言語を拡張していくための土台が刷新されたフェーズに入りました。

この土台の変化は、これまで実現が難しかった言語機能を、より現実的に議論・試作できる環境が整ったことを意味します。

そして Kotlin の進化は、JetBrainsの一方的な決定ではなく、コミュニティとともに設計を進める提案プロセス「KEEP(Kotlin Evolution and Enhancement Process)」を中心に議論されます。KEEP は採用されることが目的というより、言語仕様の選択肢を開き、設計を磨き、実装可能性を検証するための場です。

この記事では、そんなKEEPの中でも

  • 知っているとKotlinの見え方が変わる
  • まだ広く知られていないものの、特定のユースケースで非常に強力な恩恵をもたらす

そんなプロポーザルを5つ、モチベーションと提案されている解決策のセットで紹介します。


読む前の注意(重要)

  • これらは実装や安定版入りが約束されているわけではありません
  • 構文・仕様・名称は今後変わる可能性があります
  • 状況は公式の一覧(Language features & proposals)に随時整理されるため、最新の状態は必ず一次情報で確認してください

※Experimental / Previewはコンパイラフラグが必要になることがあります(有効化方法や対象バージョンは変わり得ます)。


本稿で扱う 5 つの提案(ステータスは目安)

提案 ねらい ステータス(目安)
Explicit backing fields バッキングプロパティのボイラープレート削減 In preview(実装あり・要フラグ)
Context-sensitive resolution 文脈が明確な場面で型名反復を省略 In preview(実装あり・要フラグ)
Static extensions 型(名前空間)への“静的”拡張の検討 KEEP Discussion(プロポーザルのみ)
Name-based destructuring 分解宣言の順序依存を減らしリファクタ耐性向上 KEEP Discussion(プロポーザルのみ)
Rich Errors(型付けされたエラー) 型安全な失敗表現を言語として模索 KEEP Discussion(プロポーザルのみ)

1. Explicit Backing Fields

背景と課題

AndroidのViewModelやデータレイヤーでは、不変の公開型(StateFlow)を表に出しつつ、内部では可変(MutableStateFlow)で更新する バッキングプロパティ(Backing Property)パターンが一般的です。

// 従来:同じ状態を2つのプロパティで管理
private val _items = MutableStateFlow<List<Item>>(emptyList())
val items: StateFlow<List<Item>> get() = _items

fun add(item: Item) {
    _items.value = _items.value + item
}

ただ、この手法は公開用と更新用を二重に宣言する必要があり、ボイラープレートが増えがちです。

提案される解決策

Explicit backing fields は、公開型のプロパティ宣言に対してフィールド側の型と初期化式を明示し、

公開型と実装型を 1つの識別子に集約できる方向性です。

// イメージ:公開型は StateFlow、フィールドは MutableStateFlow を持つ
val items: StateFlow<List<Item>> field = MutableStateFlow(emptyList())

fun add(item: Item) {
    // 同一スコープ内では、実体(field側)として扱えるため更新可能
    items.value = items.value + item
}

注:有効化にはコンパイラオプションが必要になる場合があります(例:-Xexplicit-backing-fields)。

技術的価値

  • バッキングプロパティの二重宣言を削減し、クラスのノイズを減らす
  • 公開API(不変)と実装(可変)の関係を言語機能で素直に表現できる
  • 同一スコープ内の扱いなど、スマートキャスト的な体験向上が期待できる

2. Context-sensitive Resolution

背景と課題

enum や sealed を扱う when では、文脈から型が明確でも型名の反復が発生し、視覚的ノイズになります。

// 現状:型名が繰り返される
when (state) {
    UIState.Loading -> ...
    UIState.Success -> ...
    UIState.Error -> ...
}

解決策:期待型が分かる場面で型名を省略

プレビュー実装では、when の分岐など期待型が確定できる箇所で、列挙子名などを型名なしで直接書く方向が示されています。

enum class Problem { CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN }

fun message(problem: Problem) = when (problem) {
    CONNECTION -> "connection"
    AUTHENTICATION -> "authentication"
    DATABASE -> "database"
    UNKNOWN -> "unknown"
}

注:有効化にはコンパイラオプションが必要になる場合があります(-Xcontext-sensitive-resolution)。

技術的価値

  • 宣言的UIやDSLで型より値へ集中できる
  • when の視認性が上がり、差分レビューや保守がしやすくなる

3. Static Extensions

背景と課題

Kotlinの拡張関数は強力ですが、基本はインスタンスが対象です。サードパーティの型に、ファクトリやパーサといった静的なユーティリティを生やしたい場合、Utils クラスに逃げることが多く、API発見性が落ちます。

object StringUtils {
    fun fromJson(json: String): User = ...
}

val user = StringUtils.fromJson(json)

方向性(検討中)

Static extensions は型そのもの(名前空間)へ拡張を与える発想です。

ただし、提案は継続的に更新が続いており、最終構文は流動的です。

// 方向性のイメージ(構文は仮)
// String.fromJson(...) のように、型名の下に集約して発見性を上げたい
val user = String.fromJson(json)

技術的価値

  • ユーティリティ散在を減らし、APIを型の文脈に集約できる
  • Javaの静的メソッド文化との親和性向上

4. Name-based Destructuring

背景と課題

現行の分解宣言は宣言順序に依存します。データクラスのプロパティ順序を変更すると、コンパイルは通るまま代入先が入れ替わるリスクがあります。

// 順序依存。同じ型だと順序を変更してもコンパイルエラーすら起こらない
val (name, email) = user

解決策:名前ベースで束縛(検討中)

変数名(または明示指定)に基づいて束縛できれば、順序変更に強くなります。

// 方向性のイメージ(構文は仮・議論中)
val (name = user.name, email = user.email) = user

// あるいは、取りたい要素だけ明示
val (email = email) = user

技術的価値

  • 順序に依存しないため、大規模リファクタに強い
  • 必要なプロパティだけ抜く意図が明確になり、保守性が上がる

5. Rich Errors

背景と課題:既存手法の限界

Kotlinのエラーハンドリングは大きく

  • 例外(Exception)
  • Result / Either 等でラップ

の2系統に寄りがちです。

  • Result は Throwable ベースの相互運用に強い一方、ドメインエラーを型として表現しづらい
  • Either は表現力が高い一方、ラッパー導入・学習コストが増える

提案される方向性(設計探索)

KEEPでは、いわゆる型付けされたエラーを言語レベルで扱う可能性が議論されています。

ここで重要なのはどこでも自由に使えるUnion型(TypeScriptのように「AまたはB」のどちらかになり得る型)が入ると断定するより、

失敗を型として扱い、分岐を自然に書ける仕組みを模索している、という捉え方です。

※以下のコードは概念を示すための擬似コードです(実際の構文は未定)

// 概念的なイメージ(実際の構文は未定):成功 or 複数の失敗型を戻り値で表したい
fun fetchUser(id: String): User | NetworkError | UserNotFound

val result = fetchUser("123")
when (result) {
    is User -> println("Welcome, ${result.name}")
    is NetworkError -> retry(result.attemptCount)
    is UserNotFound -> redirectToSignup()
}

既存手法との棲み分け(考え方)

  • Typed errors(仮):分岐が多いドメインエラーを型で表し、when とスマートキャストで自然に扱いたい
  • Result:Throwable文化のAPI相互運用、軽量な例外伝播
  • Either:エラー蓄積、関数型合成など高度な合成が必要な領域

(構想)合併型に対する拡張関数の可能性

もし言語レベルで合併型が扱えるなら、合併型に対する map のような操作も夢が広がります。

ただし、これは現時点では思考実験であり、実装・仕様が存在するわけではありません。

// あくまで思考実験(構想)
fun <T, E, R> (T | E).map(transform: (T) -> R): R | E =
    if (this is T) transform(this) else this as E

val nameResult = fetchUser("123").map { it.name }

さいごに

今回紹介した5つの提案は、単なる記法改善だけではありません。K2という刷新された基盤の上で、Kotlinが安全性と表現力の新しい均衡点を探る試みです。

  • ボイラープレートの削減(Explicit backing fields)
  • 認知負荷の低減(Context-sensitive resolution)
  • 名前空間の再編と発見性(Static extensions)
  • バグ防止・リファクタ耐性(Name-based destructuring)
  • 失敗を型で扱う設計(Rich Error)

KEEPは採用が約束されない一方で、議論を追うこと自体が言語設計とエコシステム理解の最短距離でもあります。今後も公式の提案一覧やKEEPリポジトリを起点に、動向を注視していきましょう。


参考

株式会社ログラス テックブログ

Discussion