Share
Embed
Context parameters で作る!はじめての型クラス
munetoshi / 株式会社ナレッジワーク @mntsh
2026/11/14「Kotlin Fest 2026」での登壇資料です
https://2026.kotlinfest.dev/
拡張関数 Foo.show() と Bar.show() があるとしましょう。このとき、Foo と Bar をまとめて fun showTwice(x: ???) { x.show(); x.show() } のように取り扱えたら便利だと思いませんか? それ、実現できます。そう、context parameters ならね。
Context parameters は Kotlin 2.4.0 で安定版として利用可能になり、特に依存関係の注入やスコープの制限を実現する手段として注目を集めています。しかし、それ以外にも「既存のコードに手を加えることなく、異なるクラスをまとめて取り扱う」という強力な使い方もできるのです。
本発表では、その根底にある「型クラス」という概念から丁寧に解説し、context parameters を使った新しい書き方を紹介します。
想定聴講者: Kotlinを業務・個人開発で使用し、より深く理解したい方(脱初級者を目指す方)
KNOWLEDGE WORK / 株式会社ナレッジワーク PRO
November 13, 2026
Transcript
65 slides
-
Kotlin の拡張関数 既存コードを変更せずに実装を追加できる class Foo(val name: String) class Bar(val id:
Int) fun Foo.show() = println("Foo($name)") fun Bar.show() = println("Bar(#$id)") Foo("Kotlin Fest").show() 2 -
DEMO 拡張関数で show() を足す class Foo(val name: String) class Bar(val
id: Int) fun Foo.show() = println("Foo($name)") fun Bar.show() = println("Bar(#$id)") Foo("Kotlin Fest").show() 実行 3 -
拡張関数の限界 共通の親がないと呼び出しをまとめられない class Foo(val name: String) class Bar(val id: Int)
fun Foo.show() = println("Foo($name)") fun Bar.show() = println("Bar(#$id)") showTwice(Foo("Kotlin Fest")) showTwice(Bar(2026)) fun showTwice(x: ???) { x.show() x.show() } 4 -
DEMO 拡張関数で showTwice を書いてみる class Foo(val name: String) class Bar(val
id: Int) fun Foo.show() = println("Foo($name)") fun Bar.show() = println("Bar(#$id)") showTwice(Foo("Kotlin Fest")) showTwice(Bar(2026)) fun showTwice(x: ???) { x.show() x.show() } 実行 5 -
DEMO Context parameters で showTwice を書く class Foo(val name: String)
class Bar(val id: Int) context(FooShow) { showTwice(Foo("Kotlin Fest")) } context(BarShow) { showTwice(Bar(2026)) } context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } interface Show<T> { fun show(value: T) } ⋯ 実行 7 -
親クラス/インターフェース の継承 子が親に依存する 子が親の定義を継承する Number 親クラス/インターフェース fun isZero(): Boolean 継承
Int 子クラス fun isZero() = this == 0 16 -
型クラスの適合 型クラスと型は互いに独立 実装は 型クラスのインスタンス で定義 Number<T> 型クラス Int 型 fun
isZero(x: T): Boolean IntNumber : Number<Int> 型クラスのインスタンス fun isZero(x: Int) = x == 0 17 -
型クラスの使い所 型に変更を加えられない・加えたくない + 関数の呼び出しを共通化したいとき - 外部で定義された型: フレームワークやライブラリの型 - 型の本質とは異なる機能: 表示・永続化・ハッシュ計算
- 状況により変わり得る実装: 比較・ソート順 19 -
型クラスを採用した言語 論文: How to make ad-hoc polymorphism less ad hoc
Philip Wadler, Stephen Blott (1989) - Haskell: class と instance - Scala: trait と given/implicit - Rust: trait と impl - Kotlin (NEW!): context parameters 20 -
Context parameters の基本 関数に値を渡す方法の 1 つ - context で型と値を指定 -
Kotlin 2.4.0 で安定版に 関数の引数に似ている 23 -
引数との比較 引数 greet("Kotlin Fest", ConsoleLogger) fun greet( name: String, logger:
Logger ) { logger.log("Hello, $name!") } Context parameters context(ConsoleLogger) { greet("Kotlin Fest") } context(logger: Logger) fun greet(name: String) { logger.log("Hello, $name!") } 24 -
Context parameters の利点 同じ値を多くの関数に渡す/呼び出しをまたいで渡すときに便利 context(ConsoleLogger) { // greet("Kotlin Fest") greet("Kotlin
World") } 共通する値を一箇所で指定できる またいで渡すだけなら無名にできる context(_: Logger) // fun greet(name: String) { sayHello(name) sayThanks(name) } 25 -
Context parameters の使い所: 依存性の注入 依存対象を呼び出し元から context で渡す object ConsoleLogger :
Logger { override fun log(text: String) = println(text) } class ListLogger : Logger { val logs = mutableListOf<String>() override fun log(text: String) { logs += text } } プロダクションコード // context(ConsoleLogger) { greet("Kotlin Fest") } テストコード // val logger = ListLogger() context(logger) { greet("Kotlin Fest") } 27 -
DEMO Context parameters の使い所: 依存性の注入 object ConsoleLogger : Logger {
override fun log(text: String) = println(text) } class ListLogger : Logger { val logs = mutableListOf<String>() override fun log(text: String) { logs += text } } プロダクションコード // context(ConsoleLogger) { greet("Kotlin Fest") } テストコード // val logger = ListLogger() context(logger) { greet("Kotlin Fest") } 実行 28 -
Context parameters の使い所: スコープの制限 あるスコープの中でだけ呼べる関数を作る context(tx: Transaction) fun insertUser(name: String)
= tx.insert("users", name) fun <T> transaction(block: context(Transaction) () -> T): T { val tx = Transaction() try { return context(tx) { block() } } finally { tx.close() } } transaction { insertUser("Kotlin Fest") } insertUser("Kotlin Fest") // OK // コンパイルエラー 30 -
DEMO Context parameters の使い所: スコープの制限 context(tx: Transaction) fun insertUser(name: String)
= tx.insert("users", name) fun <T> transaction(block: context(Transaction) () -> T): T { val tx = Transaction() try { return context(tx) { block() } } finally { tx.close() } } transaction { insertUser("Kotlin Fest") } insertUser("Kotlin Fest") // OK // コンパイルエラー 実行 31 -
Context parameters で作る型クラス Show<T> 型クラス Foo 型 fun show(value: T)
FooShow : Show<Foo> 型クラスのインスタンス fun show(value: Foo) = println("Foo(${value.name})") 実行には「型クラスのインスタンス」が必要 Context parameters で型クラスのインスタンスを指定する 35 -
型クラスを実装する 3 ステップ Show<T> FooShow : Show<Foo> 型クラス Foo 型
型クラスのインスタンス 1. 型クラスを interface で定義 2. 型クラスのインスタンスを object で定義 3. 型クラスのインスタンスを context で受け渡し 36 -
最初のデモのコードを読み解く 目的: Foo に show を追加する class Foo(val name: String)
型 interface Show<T> { fun show(value: T) } object FooShow : Show<Foo> { override fun show(value: Foo) = println("Foo(${value.name})") } context(s: Show<T>) fun <T> T.show() = s.show(this) context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } context(FooShow) { showTwice(Foo("Kotlin Fest")) } 37 -
最初のデモのコードを読み解く 1. 型クラスを interface で定義 class Foo(val name: String) interface
Show<T> { fun show(value: T) } object FooShow : Show<Foo> { override fun show(value: Foo) = println("Foo(${value.name})") } context(s: Show<T>) fun <T> T.show() = s.show(this) context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } context(FooShow) { showTwice(Foo("Kotlin Fest")) } 型 型クラス 37 -
最初のデモのコードを読み解く 2. 型クラスのインスタンスを object で定義 class Foo(val name: String) interface
Show<T> { fun show(value: T) } object FooShow : Show<Foo> { override fun show(value: Foo) = println("Foo(${value.name})") } context(s: Show<T>) fun <T> T.show() = s.show(this) context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } context(FooShow) { showTwice(Foo("Kotlin Fest")) } 型 型クラス 型クラスの インスタンス 37 -
最初のデモのコードを読み解く 3. 型クラスのインスタンスを context で受け渡し class Foo(val name: String) interface
Show<T> { fun show(value: T) } object FooShow : Show<Foo> { override fun show(value: Foo) = println("Foo(${value.name})") } context(s: Show<T>) fun <T> T.show() = s.show(this) context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } context(FooShow) { showTwice(Foo("Kotlin Fest")) } 型 型クラス 型クラスの インスタンス 受け取る 渡す 37 -
最初のデモのコードを読み解く 4. 一工夫: s.show(x) を x.show() と書くための拡張関数を定義 class Foo(val name:
String) interface Show<T> { fun show(value: T) } object FooShow : Show<Foo> { override fun show(value: Foo) = println("Foo(${value.name})") } context(s: Show<T>) fun <T> T.show() = s.show(this) context(_: Show<T>) fun <T> showTwice(x: T) { x.show() x.show() } context(FooShow) { showTwice(Foo("Kotlin Fest")) } 型 型クラス 型クラスの インスタンス 一工夫 受け取る 渡す 37 -
実践: 順序 Order の実装 目的: Task に順序の定義を追加する data class Task(val
id: Int, val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) println(takeFirst(task1, task2)) 38 -
実践: 順序 Order の実装 1. 型クラスを interface で定義 data class
Task(val id: Int, val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) interface Order<T> { fun compare(a: T, b: T): Int } println(takeFirst(task1, task2)) 38 -
実践: 順序 Order の実装 2. 型クラスのインスタンスを object で定義 data class
Task(val id: Int, val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) interface Order<T> { fun compare(a: T, b: T): Int } object TaskOrderById : Order<Task> { override fun compare(a: Task, b: Task) = a.id.compareTo(b.id) } println(takeFirst(task1, task2)) 38 -
実践: 順序 Order の実装 3. 型クラスのインスタンスを context で受け渡し data class
Task(val id: Int, val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) interface Order<T> { fun compare(a: T, b: T): Int } object TaskOrderById : Order<Task> { override fun compare(a: Task, b: Task) = a.id.compareTo(b.id) } context(o: Order<T>) fun <T> takeFirst(a: T, b: T): T = if (o.compare(a, b) < 0) a else b context(TaskOrderById) { println(takeFirst(task1, task2)) } 38 -
実践: 順序 Order の実装 4. 一工夫: o.compare(a, b) < 0
を a < b と書く拡張関数を定義 data class Task(val id: Int, val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) interface Order<T> { fun compare(a: T, b: T): Int } object TaskOrderById : Order<Task> { override fun compare(a: Task, b: Task) = a.id.compareTo(b.id) } context(o: Order<T>) operator fun <T> T.compareTo(other: T) = o.compare(this, other) context(_: Order<T>) fun <T> takeFirst(a: T, b: T): T = if (a < b) a else b context(TaskOrderById) { println(takeFirst(task1, task2)) } 38 -
DEMO 実践: 順序 Order の実装 data class Task(val id: Int,
val priority: Priority, val deadline: LocalDate) val task1 = Task(1, Priority.LOW, LocalDate.of(2026, 11, 20)) val task2 = Task(2, Priority.HIGH, LocalDate.of(2026, 11, 30)) interface Order<T> { fun compare(a: T, b: T): Int } object TaskOrderById : Order<Task> { ⋯ object TaskOrderByPriority : Order<Task> { override fun compare(a: Task, b: Task) = compareValuesBy(a, b, { it.priority }, { it.deadline }) } context(o: Order<T>) operator fun <T> T.compareTo(other: T) = o.compare(this, other) context(_: Order<T>) ⋯ context(TaskOrderByPriority) { println(takeFirst(task1, task2)) } 実行 39 -
Wrapper による順序の実装 Comparable を継承したクラスを作る class ComparableTask(val task: Task) : Comparable<ComparableTask>
{ override fun compareTo(other: ComparableTask) = task.id.compareTo(other.task.id) } minOf(ComparableTask(task1), ComparableTask(task2)).task 41 -
型クラスの利点: 振る舞いを合成できる Wrapper: 振る舞いの組み合わせ毎に定義が必要 fun <T> showFirst(a: T, b: T)
where T : Showable, T : Comparable<T> = minOf(a, b).show() class ShowableComparableTask(val task: Task) : Showable, Comparable<ShowableComparableTask> { override fun show() = println("Task #${task.id}") override fun compareTo(other: ShowableComparableTask) = task.id.compareTo(other.task.id) } showFirst(ShowableComparableTask(task1), ShowableComparableTask(task2)) 43 -
型クラスの利点: 振る舞いを合成できる 型クラス: 振る舞いの組み合わせにはインスタンスを渡すだけで十分 context(_: Show<T>, _: Order<T>) fun <T>
showFirst(a: T, b: T) = takeFirst(a, b).show() // 振る舞いの組み合わせには、型クラスのインスタンスは不要 context(TaskShow, TaskOrderById) { showFirst(task1, task2) } 44 -
型クラスの利点: 値を unwrap せずに使える Wrapper: 値の利用には unwrap が必要 class ComparableTask(val
task: Task) : Comparable<ComparableTask> { override fun compareTo(other: ComparableTask) = task.id.compareTo(other.task.id) } val first = minOf(ComparableTask(task1), ComparableTask(task2)) println(first.task.deadline) // unwrap が必要 46 -
型クラスの利点: 値を unwrap せずに使える 型クラス: 値を直接利用可能 context(_: Order<T>) fun <T>
takeFirst(a: T, b: T): T = if (a < b) a else b val first = context(TaskOrderById) { takeFirst(task1, task2) } println(first.deadline) // 直接使える 47 -
注意点: 型クラスのインスタンスは静的に決まる 特に継承との組み合わせに注意 open class Parent(val name: String) class Child(name:
String) : Parent(name) object ParentShow : Show<Parent> { ⋯ object ChildShow : Show<Child> { ⋯ context(ParentShow, ChildShow) { val argument: Parent = Child("Kotlin Fest") showTwice(argument) } 49 -
DEMO 注意点: 型クラスのインスタンスは静的に決まる open class Parent(val name: String) class Child(name:
String) : Parent(name) object ParentShow : Show<Parent> { ⋯ object ChildShow : Show<Child> { ⋯ context(ParentShow, ChildShow) { showTwice(Child("Kotlin Fest")) } 実行 50 -
Context parameters で作る型クラス: まとめ 実装方法 - 型クラス: interface で定義 -
型クラスのインスタンス: object で実装・context で受け渡し 利点と注意点 - 合成が簡単・値の unwrap が不要 - 型クラスのインスタンスは静的に解決 51 -
アジェンダ 1. 型クラスとは何か 2. Context parameters の基本 3. Context parameters
で作る型クラス 4. 多相性の世界のもっと深みへ 52 -
もっと深みへ: 高階カインド型 (HKT) Haskell や Scala の型クラスはもっと強力 interface Mappable<T> {
fun <P, R> T<P>.map(f: (P) -> R): T<R> } 型パラメータ T に T<P> と型引数を渡せる Kotlin では単純には実装不可能 詳しくは「高階カインド型 (HKT)」で 53 -
もっと深みへ: さまざまな多相性 代表的な多相性 - サブタイプ多相: 継承 - パラメトリック多相: ジェネリクス -
アドホック多相: オーバーロード・拡張関数・型クラス 多相性はほかにも - 行多相: アップキャストしない構造的型付け - ポリタイピズム: 型の構造からの関数の導出 54 -
まとめ Context parameters を使って型クラスを実装した → 既存のコードを変更せず 振る舞いを加え 呼び出しを共通化した 型クラス: 同じ性質の型のグループ
Context parameters: 関数に値を渡す方法 実装方法: 型クラスのインスタンスを context で受け渡し 55