Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin 2.2が切り拓く: コンテキストパラメータで書く関数型DSLと新しい依存管理のかたち

Kotlin 2.2が切り拓く: コンテキストパラメータで書く関数型DSLと新しい依存管理のかたち

Kotlin Fest 2025の資料です

Avatar for Kenichi SUZUKI

Kenichi SUZUKI

November 01, 2025
Tweet

More Decks by Kenichi SUZUKI

Other Decks in Programming

Transcript

  1. ⾃⼰紹介 鈴⽊ 健⼀ (Kenichi Suzuki) X: @_knih 新卒でNTTデータに入社。ミッションクリティカルシステム・大 規模基幹システムのアーキテクチャ設計や統括業務に従事し た後、より堅牢な開発を求めて、プログラム言語理論(型シス

    テム、関数型等)を研究。 
 その後、Visionalにてサイバーセキュリティ事業の立ち上げか らプロダクト開発をけん引した。ContractS株式会社にて開発 部長、プロダクト部長、技術戦略室長、VP of Developmentを 歴任。2023年ログラスへ入社。 Server-Side Kotlin Meetup Vol.10
  2. 関数に必要な値をどう渡すか 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") } } } }
  3. 関数に必要な値をどう渡すか 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") } } } }
  4. 関数に必要な値をどう渡すか 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") } } } } ❌ パラメータのバケツリレー
  5. 関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法2: レシーバーを使う(thisで渡す) class HtmlBuilderThis(val theme: Theme) { private

    val bodies = mutableListOf<String>() 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はない
  6. 関数に必要な値をどう渡すか DSLの問題:DSLで複数の値を全階層に渡したいときにどうするか ⽅法2: レシーバーを使う(thisで渡す) class HtmlBuilderThis(val theme: Theme) { private

    val bodies = mutableListOf<String>() 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つだけ (他のコンテキストが渡せない)
  7. 関数に必要な値をどう渡すか 従来の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) } } } オブジェクトの依存が明示的
  8. 関数に必要な値をどう渡すか 従来の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 依然として実行コンテキストは暗黙のまま
  9. 関数に必要な値をどう渡すか 改めて、関数に必要な値をどう渡すか 「明⽰的」「⾃動伝播」「コンパイル時検証」を同時に実現できない 方法 利点 欠点 パラメータ 明示的、型安全 バケツリレー this

    (レシーバー) 簡潔 thisは1つだけ(複数の値を渡せない) 従来のDI 自動、簡潔 実行時解決(リフレクション) 実行コンテキストは暗黙的
  10. コンテキストパラメーター コンテキストパラメーター(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<Order>) { orders.forEach { processOrder(it) validateOrder(it) saveOrder(it) } } val logger = Logger.getLogger() with(logger) { handleOrders(orderList) } Loggerは自動で伝播する 実際のLoggerを注入する
  11. コンテキストパラメーター コンテキストパラメータを使う • 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") } }
  12. ケイパビリティ合成 ケイパビリティ合成(Capability Composition) WhoではなくWhatを扱う と、名付けてみました (従来の) DI • オブジェクトを注入 •

    誰に依存?(Who) • 実行時解決 ケイパビリティ合成 • 能力(capability)を要求 • 何ができる?(What) • コンパイル時解決 関数型の世界でよく使われるアプローチが コンテキストパラメータによって実現しやすくなりました
  13. ケイパビリティ (参考)ケイパビリティの由来と関数型のパターン • 「ケイパビリティ」の由来 ◦ 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]
  14. ケイパビリティ合成 ケイパビリティを注⼊する どういったケイパビリティを持っているか、を型で⽰せる interface CanSendEmail { suspend fun send(email: String,

    message: String): Result<Unit> } context(emailSvc: CanSendEmail) suspend fun notifyUser(email: String): Result<Unit> = emailSvc.send(email, "Confirmed") メールを送信できるケイパビリティ 持っているケイパビリティが型でわかる
  15. ケイパビリティ合成 ケイパビリティは合成できる ⼩さなケイパビリティを組み合わせて、⼤きな機能をつくる context( tx: CanExecuteTransaction, audit: CanAuditLog, notif: CanNotify,

    db: CanAccessAccounts ) suspend fun transferMoney(from: String, to: String, amount: Double): Result<Unit> = 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") // ← 通知 } 口座アクセス 監査ログ、通知等々
  16. ケイパビリティ合成 ケイパビリティの垂直合成 関数呼び出しの階層におけるケイパビリティの伝播 垂直合成の場合、最終的なケイパビリティは増えない(ケイパビリティの幅は広がらない) 口座アクセス 口座アクセス 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 ... }
  17. ケイパビリティ合成 ケイパビリティの⽔平合成 複数の独⽴したケイパビリティの並列結合 ⽔平合成はケイパビリティの幅が増える 口座アクセス 監査ログ 通知 context(db: CanAccessAccounts) fun

    debitAccount(id: String, amount: Double): Result<Unit> context(audit: CanAuditLog) fun logTransaction(action: String): Result<Unit> ... context(db: CanAccessAccounts, audit: CanAuditLog, notif: CanNotify) fun transferMoney(from: String, to: String, amount: Double): Result<Unit>
  18. 関数型⾵DSL DSL例: クエリ⾔語 関数の組み合わせでプログラムがかける query(myConnection, logger) { select("name", "email", "age")

    { from("users") { where { and { equals("status", "active") greaterThan("age", 18) } } } } } これは関数 これも関数 ぜんぶ関数
  19. 関数型⾵DSL DSL例: クエリ⾔語 context(connection: Connection, logger: Logger) class QueryScope {

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

    private val conditions = mutableListOf<String>() 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()})") } ... } 条件句のなかで使える 演算子を定義 条件句のなかで使える 演算子を定義
  21. 関数型⾵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() } } } 実行コンテキストを注入
  22. 応⽤例 認証コンテキスト 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 } ... } 認証コンテキストをコンテキストパラメータ化して、シグネチャで表現する どの関数(クラス)が認証コンテキストを扱っているか⼀⽬瞭然になる 管理者権限がないと ユーザーを削除できない
  23. パフォーマンス⽐較 パフォーマンス計測の対象 今回は、⼩規模なサービスを想定 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)
  24. パフォーマンス⽐較 パフォーマンス計測‧⽐較結果 フレームワーク 内部時間 (平均) 外部時間 (平均) 内部時間 (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起動を含めた時間 より大規模なユースケースでは、性能差はさ らに広がると思われます
  25. パフォーマンス⽐較 パフォーマンス計測‧⽐較結果 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起動を含めた時間
  26. まとめ コンテキストパラメータの正式リリースに備えましょう • コンテキストパラメーターが可能にしたこと ◦ thisレシーバーの単⼀性制約から解放 ◦ 複数のコンテキストを同時に扱える ◦ 「明⽰的」「⾃動伝播」「コンパイル時検証」を同時に実現する

    • ケイパビリティ合成 ◦ ケイパビリティ⾃体は不変 ◦ ケイパビリティを合成して⼤きな機能をつくる(垂直‧⽔平) • 関数合成によるDSL構築 ◦ コンテキストパラメーターによってレシーバー関数を合成しやすくなった • 脱リフレクションすると速い ◦ Native Image化するともっと速い