Slide 1

Slide 1 text

Kotlin 2.2が切り拓く: コンテキストパラメータで書 く関数型DSLと新しい依存管理のかたち 鈴⽊ 健⼀ Kotlin Fest 2025 株式会社ログラス CTO室⻑ #KotlinFest #let

Slide 2

Slide 2 text

⾃⼰紹介 鈴⽊ 健⼀ (Kenichi Suzuki) X: @_knih 新卒でNTTデータに入社。ミッションクリティカルシステム・大 規模基幹システムのアーキテクチャ設計や統括業務に従事し た後、より堅牢な開発を求めて、プログラム言語理論(型シス テム、関数型等)を研究。 
 その後、Visionalにてサイバーセキュリティ事業の立ち上げか らプロダクト開発をけん引した。ContractS株式会社にて開発 部長、プロダクト部長、技術戦略室長、VP of Developmentを 歴任。2023年ログラスへ入社。 Server-Side Kotlin Meetup Vol.10

Slide 3

Slide 3 text

NoSuchBeanDefinitionException 循環依存エラー @Qualifier地獄 どのBeanが注入されてるか分からない Native Image化で謎のリフレクションエラー

Slide 4

Slide 4 text

世はまさに ⼤依存地獄 時代… Photo: Chris Barbalis / Unsplash

Slide 5

Slide 5 text

そこで、コンテキスト

Slide 6

Slide 6 text

Dependency Injection Capability Composition 依存注⼊ から ケイパビリティ合成 へ

Slide 7

Slide 7 text

リフレクション から 型安全‧⾼速化 へ

Slide 8

Slide 8 text

Spring vs コンテキストパラメータ 起動時の性能差 14.7倍 ※個⼈計測、M2 Pro使⽤

Slide 9

Slide 9 text

本⽇のお品書き ● 関数に必要な値をどう渡すか ● コンテキストパラメーター ● DIからケイパビリティ合成へ ● 関数型⾵DSL ● 応⽤例 ● パフォーマンス⽐較 ● まとめ

Slide 10

Slide 10 text

関数に必要な値をどう渡すか

Slide 11

Slide 11 text

関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法1: パラメータで渡す class HtmlBuilder(val theme: Theme, val logger: Logger) { fun body(block: (BodyBuilder) .> Unit) { logger.log("Creating body") // ← logger を使う val body = BodyBuilder(theme, logger) block(body) … class BodyBuilder(val theme: Theme, val logger: Logger) { fun div(block: (DivBuilder) .> Unit) { logger.log("Creating div") // ← logger を使う val div = DivBuilder(theme, logger) block(div) … class DivBuilder(val theme: Theme, val logger: Logger) { fun span(block: (SpanBuilder) .> Unit) { logger.log("Creating span") // ← logger を使う val span = SpanBuilder(theme, logger) … htmlParam(theme, logger) {builder .> builder.body { body .> body.div { div .> div.span { span .> span.text("Hello") } } } }

Slide 12

Slide 12 text

関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法1: パラメータで渡す class HtmlBuilder(val theme: Theme, val logger: Logger) { fun body(block: (BodyBuilder) .> Unit) { logger.log("Creating body") // ← logger を使う val body = BodyBuilder(theme, logger) block(body) … class BodyBuilder(val theme: Theme, val logger: Logger) { fun div(block: (DivBuilder) .> Unit) { logger.log("Creating div") // ← logger を使う val div = DivBuilder(theme, logger) block(div) … class DivBuilder(val theme: Theme, val logger: Logger) { fun span(block: (SpanBuilder) .> Unit) { logger.log("Creating span") // ← logger を使う val span = SpanBuilder(theme, logger) … htmlParam(theme, logger) {builder .> builder.body { body .> body.div { div .> div.span { span .> span.text("Hello") } } } }

Slide 13

Slide 13 text

関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法1: パラメータで渡す class HtmlBuilder(val theme: Theme, val logger: Logger) { fun body(block: (BodyBuilder) .> Unit) { logger.log("Creating body") // ← logger を使う val body = BodyBuilder(theme, logger) block(body) … class BodyBuilder(val theme: Theme, val logger: Logger) { fun div(block: (DivBuilder) .> Unit) { logger.log("Creating div") // ← logger を使う val div = DivBuilder(theme, logger) block(div) … class DivBuilder(val theme: Theme, val logger: Logger) { fun span(block: (SpanBuilder) .> Unit) { logger.log("Creating span") // ← logger を使う val span = SpanBuilder(theme, logger) … htmlParam(theme, logger) {builder .> builder.body { body .> body.div { div .> div.span { span .> span.text("Hello") } } } } ❌ パラメータのバケツリレー

Slide 14

Slide 14 text

関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法2: レシーバーを使う(thisで渡す) class HtmlBuilderThis(val theme: Theme) { private val bodies = mutableListOf() fun body(block: BodyScope.() .> Unit) { val builder = BodyScope(theme) builder.block() bodies.add(builder.build()) } … class BodyScope(val theme: Theme) { … fun div(block: DivScope.() .> Unit) { val builder = DivScope(theme) … } class DivScope(val theme: Theme) { … } html(theme) { body { div { span { // theme にはアクセスできる val color = this.theme.color println("Color: $color") text("Content") } } } } theme にはアクセスできるが、 loggerはない

Slide 15

Slide 15 text

関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法2: レシーバーを使う(thisで渡す) class HtmlBuilderThis(val theme: Theme) { private val bodies = mutableListOf() fun body(block: BodyScope.() .> Unit) { val builder = BodyScope(theme) builder.block() bodies.add(builder.build()) } … class BodyScope(val theme: Theme) { … fun div(block: DivScope.() .> Unit) { val builder = DivScope(theme) … } class DivScope(val theme: Theme) { … } html(theme) { body { div { span { // theme にはアクセスできる val color = this.theme.color println("Color: $color") text("Content") } } } } theme にはアクセスできるが、 loggerはない ❌ レシーバーは常に1つだけ (他のコンテキストが渡せない)

Slide 16

Slide 16 text

関数に必要な値をどう渡すか 値をどう渡すか、という問題 複数の値を全階層に渡したい 方法1: パラメータで渡す バケツリレー 方法2: thisで渡す thisは1つのみ

Slide 17

Slide 17 text

関数に必要な値をどう渡すか 従来のDIが解決したこと、解決していないこと @Service class OrderService( private val userRepository: UserRepository, private val paymentGateway: PaymentGateway, private val orderRepository: OrderRepository ) { fun processOrder(order: Order): Boolean { // userの存在確認 userRepository.findById(order.userId) .: return false // 支払い処理して、成功したら保存 return paymentGateway.charge(order.amount).also { success .> if (success) orderRepository.save(order) } } } オブジェクトの依存が明示的

Slide 18

Slide 18 text

関数に必要な値をどう渡すか 従来のDIが解決したこと、解決していないこと @Service class OrderService( private val userRepository: UserRepository, private val paymentGateway: PaymentGateway, private val orderRepository: OrderRepository ) { fun processOrder(order: Order): Boolean { val currentUser = SecurityContextHolder .getContext().authentication.principal logger.info("Processing order ${order.id}") … LoggerFactory.getLogger() SecurityContextHolder @Transactional 依然として実行コンテキストは暗黙のまま

Slide 19

Slide 19 text

関数に必要な値をどう渡すか 従来のDIが解決したこと、解決していないこと オブジェクトの依存 コンストラクタに現れる 実行コンテキスト (Logger, Config…) 関数シグネチャに現れない

Slide 20

Slide 20 text

関数に必要な値をどう渡すか 改めて、関数に必要な値をどう渡すか 「明⽰的」「⾃動伝播」「コンパイル時検証」を同時に実現できない 方法 利点 欠点 パラメータ 明示的、型安全 バケツリレー this (レシーバー) 簡潔 thisは1つだけ(複数の値を渡せない) 従来のDI 自動、簡潔 実行時解決(リフレクション) 実行コンテキストは暗黙的

Slide 21

Slide 21 text

コンテキストパラメーター

Slide 22

Slide 22 text

コンテキストパラメーター コンテキストパラメーター(Context Parameters)とは 関数に必要なコンテキストを明⽰的に宣⾔し、⾃動的に伝播させる仕組み context(logger: Logger) fun processOrder(order: Order) { logger.info("Processing: ${order.id}") } context(logger: Logger) fun validateOrder(order: Order) { … } ... context(logger: Logger) fun handleOrders(orders: List) { orders.forEach { processOrder(it) validateOrder(it) saveOrder(it) } } val logger = Logger.getLogger() with(logger) { handleOrders(orderList) } Loggerは自動で伝播する 実際のLoggerを注入する

Slide 23

Slide 23 text

コンテキストパラメーター コンテキストパラメータを使う ● Kotlin 2.1.20でpartial support、2.2 でβ版になった ● Experimental機能のため、使うにはcontent-parameters オプションを有効化する ● (参考) KEEP-367: https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md ● kotlin { jvmToolchain(21) compilerOptions { freeCompilerArgs.add("-Xcontext-parameters") } }

Slide 24

Slide 24 text

コンテキストパラメーター コンテキストパラメータと他の⼿法の⽐較 「明⽰的」「⾃動伝播」「コンパイル時検証」を同時に実現する 方法 明示的? 自動伝播? コンパイル時検証? パラメータ ◯ ☓ ◯ this (レシーバー) ☓ ◯ △ 従来のDI △ ◯ ☓ コンテキストパラメー ター ◯ ◯ ◯

Slide 25

Slide 25 text

ケイパビリティ合成 ⽔平合成と垂直合成

Slide 26

Slide 26 text

ケイパビリティ合成 ケイパビリティ合成(Capability Composition) WhoではなくWhatを扱う と、名付けてみました (従来の) DI ● オブジェクトを注入 ● 誰に依存?(Who) ● 実行時解決 ケイパビリティ合成 ● 能力(capability)を要求 ● 何ができる?(What) ● コンパイル時解決 関数型の世界でよく使われるアプローチが コンテキストパラメータによって実現しやすくなりました

Slide 27

Slide 27 text

ケイパビリティ (参考)ケイパビリティの由来と関数型のパターン ● 「ケイパビリティ」の由来 ○ Object Capability Model(OCM) ○ オブジェクトへの参照を持つ = 操作権限を持つ ○ 参照がなければ操作できない ○ 最⼩権限の原則を⾃然に実現 ● 関数型パターン: Reader Monad、ZIO等を使うときによく現れるパターン ○ これをケイパビリティ合成と呼んだ file = open(“data.csv”) → file はオブジェクトの参照を持つの で操作権限を持つ // Scala ZIO def transferMoney(from: String, to: String, amount: Double) : ZIO[Database & Logger & AuthContext, AppError, Unit]

Slide 28

Slide 28 text

ケイパビリティ合成 従来のDI:依存を注⼊する @Service class OrderService( private val userRepository: UserRepository, private val paymentGateway: PaymentGateway ) オブジェクトを注入する

Slide 29

Slide 29 text

ケイパビリティ合成 ケイパビリティを注⼊する どういったケイパビリティを持っているか、を型で⽰せる interface CanSendEmail { suspend fun send(email: String, message: String): Result } context(emailSvc: CanSendEmail) suspend fun notifyUser(email: String): Result = emailSvc.send(email, "Confirmed") メールを送信できるケイパビリティ 持っているケイパビリティが型でわかる

Slide 30

Slide 30 text

ケイパビリティ合成 ケイパビリティ合成の3原則(特徴) 1. イミュータブル ケイパビリティは状態を持たない 状態を持たないことにより副作⽤が少なく推論しやすくなる 2. 合成可能 ⼩さなケイパビリティを組み合わせて⼤きな機能をつくる 3. 型安全 コンパイル時になにができるかがわかる 型安全だからこそ、実装の差し替えが容易

Slide 31

Slide 31 text

ケイパビリティ合成 ケイパビリティは合成できる ⼩さなケイパビリティを組み合わせて、より⼤きなケイパビリティや機能をつくれる 監査ログ 通知 トランザクション 口座アクセス 送金

Slide 32

Slide 32 text

ケイパビリティ合成 ケイパビリティは合成できる ⼩さなケイパビリティを組み合わせて、⼤きな機能をつくる context( tx: CanExecuteTransaction, audit: CanAuditLog, notif: CanNotify, db: CanAccessAccounts ) suspend fun transferMoney(from: String, to: String, amount: Double): Result = tx.transaction { debitAccount(from, amount) // ← 能力1 .flatMap { creditAccount(to, amount) } // ← 能力2 }.flatMap { audit.log("transfer", from, "Transferred $$amount to $to") // ← 監査ログ }.flatMap { notif.send(to, "You received $$amount") // ← 通知 } 口座アクセス 監査ログ、通知等々

Slide 33

Slide 33 text

ケイパビリティ合成 ケイパビリティの垂直合成 関数呼び出しの階層におけるケイパビリティの伝播 垂直合成の場合、最終的なケイパビリティは増えない(ケイパビリティの幅は広がらない) 口座アクセス 口座アクセス context(db: CanAccessAccounts) fun debitAccount(id: String, amount: Double) = ... context(db: CanAccessAccounts) fun creditAccount(id: String, amount: Double) = ... context(db: CanAccessAccounts) fun transferMoney(from: String, to: String, amount: Double) ... { debitAccount(from, amount) // ← 自動伝播 creditAccount(to, amount) // ← 自動伝播 return ... }

Slide 34

Slide 34 text

ケイパビリティ合成 ケイパビリティの⽔平合成 複数の独⽴したケイパビリティの並列結合 ⽔平合成はケイパビリティの幅が増える 口座アクセス 監査ログ 通知 context(db: CanAccessAccounts) fun debitAccount(id: String, amount: Double): Result context(audit: CanAuditLog) fun logTransaction(action: String): Result ... context(db: CanAccessAccounts, audit: CanAuditLog, notif: CanNotify) fun transferMoney(from: String, to: String, amount: Double): Result

Slide 35

Slide 35 text

関数型 ⾵ DSL

Slide 36

Slide 36 text

関数型⾵DSL 関数型DSL ここでは、関数を組み合わせて構築する内部DSLを関数型DSLと呼ぶことにします ● トップレベル関数で構築 ● contextで複数のコンテキストを扱う ● 小さな関数を組み合わせて大きなDSLを構成 ● (イミュータブルで)合成可能 Kotlin DSLらしさを保つなら、 内部はミュータブルなビルダーを持つ必要がある

Slide 37

Slide 37 text

関数型⾵DSL DSL例: クエリ⾔語 関数の組み合わせでプログラムがかける query(myConnection, logger) { select("name", "email", "age") { from("users") { where { and { equals("status", "active") greaterThan("age", 18) } } } } }

Slide 38

Slide 38 text

関数型⾵DSL DSL例: クエリ⾔語 関数の組み合わせでプログラムがかける query(myConnection, logger) { select("name", "email", "age") { from("users") { where { and { equals("status", "active") greaterThan("age", 18) } } } } } これは関数 これも関数 ぜんぶ関数

Slide 39

Slide 39 text

関数型⾵DSL DSL例: クエリ⾔語 context(connection: Connection, logger: Logger) class QueryScope { private val queries = mutableListOf() fun select(vararg columns: String, block: SelectScope.() .> Unit) { logger.info("Creating SELECT") val scope = SelectScope(columns.toList()) scope.block() queries.add(scope.build()) } } レシーバー ビルダー 実行コンテキストをコンテキストパラメータ化 することで、レシーバーによる DSLの構成に 集中できる

Slide 40

Slide 40 text

関数型⾵DSL DSL例: クエリ⾔語 context(connection: Connection, logger: Logger) class WhereScope { private val conditions = mutableListOf() fun equals(column: String, value: String) { logger.info("Adding condition: $column = $value") connection.validate(column) conditions.add("$column = '$value'") } fun or(block: WhereScope.() .> Unit) { val nested = WhereScope() nested.block() conditions.add("OR (${nested.build()})") } ... } 条件句のなかで使える 演算子を定義 条件句のなかで使える 演算子を定義

Slide 41

Slide 41 text

関数型⾵DSL DSL例: クエリ⾔語 fun query(connection: Connection, logger: Logger, block: context(Connection, Logger) QueryScope.() .> Unit): String { return with(connection) { with(logger) { val scope = QueryScope() scope.block() scope.build() } } } 実行コンテキストを注入

Slide 42

Slide 42 text

コンテキストパラメーターは、 thisは1つという制約を超えて、 複数のコンテキスト(connection, logger)を同時に扱えるようにし、 レシーバー関数を⾃由に合成可能にした

Slide 43

Slide 43 text

応⽤例

Slide 44

Slide 44 text

応⽤例 認証コンテキスト 認証コンテキストをコンテキストパラメータ化して、シグネチャで表現する どの関数(クラス)が認証コンテキストを扱っているか⼀⽬瞭然になる enum class Permission { READ_PROFILE, UPDATE_PROFILE, DELETE_ACCOUNT, ADMIN } // 認証情報をコンテキストとして扱う data class AuthContext( val currentUser: User, val permissions: Set )

Slide 45

Slide 45 text

応⽤例 認証コンテキスト context(auth: AuthContext, logger: Logger) fun deleteAccount(userId: String): Boolean { // 管理者権限が必要 if (Permission.ADMIN !in auth.permissions) { logger.error("Permission denied: ADMIN permission required") return false } ... } 認証コンテキストをコンテキストパラメータ化して、シグネチャで表現する どの関数(クラス)が認証コンテキストを扱っているか⼀⽬瞭然になる 管理者権限がないと ユーザーを削除できない

Slide 46

Slide 46 text

パフォーマンス⽐較

Slide 47

Slide 47 text

パフォーマンス⽐較 パフォーマンス計測の対象 今回は、⼩規模なサービスを想定 49個のオブジェクトをインジェクションし、その起動時間を計測 - Configuration: 3個(AppConfig, DatabaseConfig, CacheConfig) - Logging and Monitoring: 3個(Logger, MetricsCollector, Tracer) - Data Access: 5個(UserRepository, OrderRepository, ProductRepository, PaymentRepository, InventoryRepository) - External Services: 5個(EmailService, SmsService, PaymentGateway, ShippingService, NotificationService) - Authentication: 3個(AuthService, AuthorizationService, TokenService) - Transaction and Persistence: 3個(TransactionManager, CacheManager, SessionManager) - Business Logic Support: 5個(ValidationService, SerializationService, EncryptionService, CompressionService, FileStorageService) - Event and Messaging: 3個(EventPublisher, MessageQueue, WebSocketService) - Scheduling and Jobs: 2個(JobScheduler, TaskExecutor) - Rate Limiting: 2個(RateLimiter, CircuitBreaker) - Search and Indexing: 2個(SearchService, RecommendationService) - Image and Media: 2個(ImageProcessor, VideoProcessor) - API and HTTP: 2個(HttpClient, ApiGateway) - Geolocation: 2個(GeocodeService, DistanceCalculator) - Analytics: 2個(AnalyticsService, ReportGenerator) - A/B Testing: 2個(FeatureFlagService, ExperimentService) - Internationalization: 2個(TimeZoneService, LocalizationService)

Slide 48

Slide 48 text

パフォーマンス⽐較 パフォーマンス計測‧⽐較結果 フレームワーク 内部時間 (平均) 外部時間 (平均) 内部時間 (vs CP) 外部時間 (vs CP) Context Params 29.83ms 159ms 1.00x (基準) 1.00x (基準) Koin 81.77ms 195ms 2.74x slower 1.23x slower Spring Core 439.87ms 566ms 14.74x slower 3.56x slower Spring Boot 895.48ms 1079ms 30.01x slower 6.79x slower 内部時間はJVM内でオブジェクトを生成・注入するのに掛かった時間 外部時間はJVM起動を含めた時間 より大規模なユースケースでは、性能差はさ らに広がると思われます

Slide 49

Slide 49 text

もっと速くしたい‧‧‧!

Slide 50

Slide 50 text

ならば

Slide 51

Slide 51 text

Native Image with GraalVM コンテキストパラメーターによって、 脱リフレクションした今ならいける・・・!

Slide 52

Slide 52 text

パフォーマンス⽐較 パフォーマンス計測‧⽐較結果 with Native Image フレームワーク 内部時間 (平均) 外部時間 (平均) 内部時間 (vs CP) 外部時間 (vs CP) Context Params (GraalVM) - 22ms N/A 1.00x (基準) Context Params 29.83ms 159ms 1.00x (基準) 7.23x slower Koin 81.77ms 195ms 2.74x slower 8.86x slower Spring Core 439.87ms 566ms 14.74x slower 25.73x slower Spring Boot 895.48ms 1079ms 30.01x slower 49.05x slower 内部時間はJVM内でオブジェクトを生成・注入するのに掛かった時間 外部時間はJVM起動を含めた時間

Slide 53

Slide 53 text

まとめ

Slide 54

Slide 54 text

まとめ コンテキストパラメータの正式リリースに備えましょう ● コンテキストパラメーターが可能にしたこと ○ thisレシーバーの単⼀性制約から解放 ○ 複数のコンテキストを同時に扱える ○ 「明⽰的」「⾃動伝播」「コンパイル時検証」を同時に実現する ● ケイパビリティ合成 ○ ケイパビリティ⾃体は不変 ○ ケイパビリティを合成して⼤きな機能をつくる(垂直‧⽔平) ● 関数合成によるDSL構築 ○ コンテキストパラメーターによってレシーバー関数を合成しやすくなった ● 脱リフレクションすると速い ○ Native Image化するともっと速い

Slide 55

Slide 55 text

No content