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

ドメインモデリングにおける抽象の役割、tagless-finalによるDSL構築、そして型安全...

 ドメインモデリングにおける抽象の役割、tagless-finalによるDSL構築、そして型安全な最適化

Avatar for Kenichi SUZUKI

Kenichi SUZUKI

June 13, 2025
Tweet

More Decks by Kenichi SUZUKI

Other Decks in Programming

Transcript

  1. もともとはキレイなドメインモデル • 明確な責務 • 予測可能な状態遷移 • 理解とメンテナンスが容易 case class FiscalPeriod(

    unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit } ビジネスモデルを明確に反映して信頼性が高い状態
  2. あるとき変更される case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int,

    var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } このときは正しかった変更 四半期末の速報のため、一時的に期間を終了 したい 監査が終了するまで、完全に閉鎖することは 避けたい 他部署のレポート待ちのため、一時的に閉じた い
  3. モデルの外部に判断が漏れる case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int,

    var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } temporaryClose は使ってよ さそう 呼び出すタイミングは状況による なぁ close()を呼び出すだけでよくな い? Uses temporaryClose() Uses both with if logic temporaryClose() Dev A Dev B Dev C そして、構造がルールを定義しなく なると、意味は失われ始める
  4. 意味を保持できなくなったモデル case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int,

    var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } temporaryClose() を使う 両方を使い分ける temporaryClose()は無視 Dev A Dev B Dev C 使われ方がバラバラ 意味を守れない構造 if (isPreliminary) period.temporaryClose() if (isAudited) period.close() else period.temporaryClose() period.close()
  5. Frederick P. Brooks, JR. 著, 滝沢徹, 牧野祐子, 富澤昇 訳. 『人月の神話』.

    丸善出版 (2014) Brooks, Fred P. (1986). “No Silver Bullet — Essence and Accident in Software Engineering”. Proceedings of the IFIP Tenth World Computing Conference: 1069–1076.
  6. 本質的複雑性 (Essential) 偶有的複雑性 (Accidental) 定義 解くべき問題そのものが持つ、避けられ ない複雑性 問題解決の過程で、道具や技術的制約によっ て付随的に生まれてしまった複雑性 発生原因

    問題領域(ドメイン) 技術的制約や解決方法、設計、実装の選択に 起因 作業 頭の中で概念構造体をつくる 実装過程 避けられるか 避けられない 回避・削減可能 例 税制ルール、為替変動 依存地獄、冗長なコード 対応方法 深いドメイン理解、正しい抽象化 適切な技術選定、設計、ツール等 本質的複雑性と偶有的複雑性 “私が本質と言っているソフトウェア構築の部分は頭の 中で概念構造体を作ることであり、 偶有的事項と言って いる部分はインプリメンテーション過程のことなのだ。 ー「人月の神話」第17章より 18
  7. 抽象 21 Abstraction is about digging deep into a situation

    to find out what is at its core making it tick. Another way to think of it is about stripping away irrelevant details, or rather, stripping away details that are irrelevant to what we’re thinking about now. 抽象とは、ある状況を深く掘り下げ、物事の核心にある「それを動かしている 本質」を見つけ出すことです。別の言い方をすれば、今考えていることにとっ て重要でない詳細を取り除いていく、あるいは不要な詳細をそぎ落としてい くことです。 “ Cheng E. The Joy of Abstraction: An Exploration of Math, Category Theory, and Life. Cambridge University Press; 2022.
  8. 抽象の役割 23 在庫管理 ドメイン 受注ドメイン 顧客管理 ドメイン 顧客管理 コンテキスト 在庫管理

    コンテキスト 受注コンテキスト 問題空間 解決空間 現実世界 ドメインモデル 抽象 問題空間と解決空間の橋渡しをする
  9. 安定依存の原則と安定度・抽象度等価の原則 24 Entities Controller G atew ay Presenter Use Cases

    UI W eb Devices D B External Interfaces Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall. 依存方向
  10. 抽象の特性:本質の抽出 (意味の核を形成する) 27 最小化 Minimalism 蒸留 Distillation 表現力 Expressiveness 意味の核以外のノイズを除去する

    ドメイン知識から本質概念を抽出し、DSLの語彙として定義する 必要なものだけを公開し、余計なものを隠す DSLに含める演算・型・操作を厳選。誤用を減らし、意味の純度を保つ 意味が明快に表現されており、意図が伝わる ドメインの意図が直接・簡潔にDSLコードに表現される
  11. 抽象の特性:構造に作用する 28 合成可能性 Composability 拡張可能性 Extensibility 直交性 Orthogonality 合成可能な構造であること。意味が壊れずに繋がる DSLの操作が安全に合成可能であること(例:式のネスト、関数合成)

    他の構成要素と独立に機能し、影響を与え合わない DSL内の各構成要素が独立しており、干渉しにくい 将来的な拡張に強く、壊れにくい 将来の新しいビジネス要件に対応する拡張ポイントを設計する
  12. 余計な詳細の例:偶有的複雑性の一例 31 class InventoryProcessor { private val validator: StockValidator =

    { try { new StockValidatorImpl(...) } catch { case ex: InitializationException => // ... 何らかのエラーハンドリング throw ex } } def process(orders: List[Order]): Unit = { orders.foreach { order => validator.validateStock(order.productId, order.quantity) } } // ... }
  13. 余計な詳細の例:偶有的複雑性の一例 32 class InventoryProcessor { private val validator: StockValidator =

    { try { new StockValidatorImpl(...) } catch { case ex: InitializationException => // ... 何らかのエラーハンドリング throw ex } } def process(orders: List[Order]): Unit = { orders.foreach { order => validator.validateStock(order.productId, order.quantity) } } // ... } 余計な詳細 偶有的な複雑さ
  14. 余計な詳細の例: ノイズを除去する 33 class InventoryProcessor[F[_]: Monad](validator: StockValidator[F]) { def process(orders:

    List[Order]): F[Unit] = orders.traverse_(order => validator.validateStock(order.productId, order.quantity) ) } 依存注入 副作用の抽象化 複雑さが減ると AIにも嬉しい! tagless-finalパターン というやつ
  15. • 型安全な埋め込み言語を構築する手法(≠tagless-fnalパターン) • DSLの型付けがホストとなる言語のそれに帰着する ◦ 型検査アルゴリズムを実装しなくてよい • HOASが使える ◦ ホストとなる言語によってスコープ安全性が担保される

    • 言語コンポーネントの合成が可能 • パーサーを作らなくて良い • ホスト言語のエコシステムが使える(IDE、AIエージェント等々) tagless-finalとは 36 Claude Codeでtagless プログラムを書くのがアツい!
  16. 対象言語を設計する: Typing Rules 51 Syntax Typing Rules 上が成立していれば 下を導くことができる n

    が整数であるならば n は整数(Int) の型を持つ e1 が整数であるならば かつ e2 が整数であるならば e1 + e2 は整数(Int) の型を持つ
  17. 対象言語を設計する: Semantics 55 n は n と評価される e1 は n1

    に評価される かつ e2 は n2 に評価される Syntax Typing Rules Semantics
  18. 対象言語を設計する: Semantics 56 n は n と評価される e1 は n1

    に評価される かつ e2 は n2 に評価される このとき、 n1 + n2 の結果は n となる Syntax Typing Rules Semantics
  19. 対象言語を設計する: Semantics 57 n は n と評価される e1 は n1

    に評価される かつ e2 は n2 に評価される このとき、 n1 + n2 の結果は n となる ならば、 e1 + e2 は n と評価される Syntax Typing Rules Semantics
  20. エンコーディング手順:まずは構文を関数で表現 65 Syntax Typing Rules trait ArithSym { def int(n:

    Int): Int def add(e1: Int, e2: Int): Int } このままでは 世界がごっちゃ になる
  21. エンコーディング手順:私たちの世界の型を表現する 68 Syntax Typing Rules trait ArithSym { type Repr[T]

    def int(n: Int): Int def add(e1: Int, e2: Int): Int } 世界を表現する型 Repr をつくる
  22. エンコーディング手順:私たちの世界の型を表現する 69 Syntax Typing Rules trait ArithSym { type Repr[T]

    def int(n: Int): Repr[Int] def add(e1: Repr[Int], e2: Repr[Int]): Repr[Int] } Reprで包む
  23. エンコーディング手順:私たちの世界の型を表現する 70 Syntax Typing Rules trait ArithSym { type Repr[T]

    def int(n: Int): Repr[Int] def add(e1: Repr[Int], e2: Repr[Int]): Repr[Int] } Z が Repr[Int] Reprで包む
  24. 意味論の実装:設計をなぞるだけ 80 Semantics object EvalInterpreter extends ArithSym { type Repr[T]

    = T def int(n: Int): Repr[Int] = n def add(e1: Repr[Int], e2: Repr[Int]): Repr[Int] = e1 + e2 } 素のまま扱うため表現型は T と置く
  25. 意味論の実装:複数のインタプリタ 81 case class Writer[A](log: List[String], value: A) { def

    map[B](f: A => B): Writer[B] = Writer(log, f(value)) def flatMap[B](f: A => Writer[B]): Writer[B] = { val next = f(value) Writer(log ++ next.log, next.value) } } object WriterInterpreter extends ArithSym { type Repr[T] = Writer[T] def int(n: Int): Repr[Int] = Writer(List(s"Creating int: $n"), n) def add(e1: Repr[Int], e2: Repr[Int]): Repr[Int] = { val v1 = e1.value val v2 = e2.value Writer(e1.log ++ e2.log :+ s"Adding $v1 and $v2", v1 + v2) } } Writerモナド この世界ではWriterとして扱う インタプリタを切り替えても、ユーザープログラムには影響しない
  26. ユーザープログラムを実行する 83 object Main extends App { // Eval example

    val exampleEval = new Example(EvalInterpreter) val result1 = exampleEval.program println(result1) // 3 // Writer example val exampleWriter = new Example(WriterInterpreter) val result2: Writer[Int] = exampleWriter.program println(s"Result: ${result2.value}") // 3 println("Logs:") result2.log.foreach(println) // Creating int: 1 // Creating int: 2 // Adding 1 and 2 } def program: sym.Repr[Int] = { val a = int(1) val b = int(2) add(a, b) } ユーザープログラムは同じ インタプリタ インタプリタ
  27. 言語を合成できる、tagless-finalならね 85 trait ArithSym { type Repr[T] def int(n: Int):

    Repr[Int] def add(...(略) } trait LamSym { type Repr[T] def lambda[A, B](f: Repr[A] => Repr[B]): Repr[A => B] def apply[A, B](f: Repr[A => B], x: Repr[A]): Repr[B] } object EvalInterpreter extends ArithSym with LamSym { type Repr[T] = T def int(n: Int): Repr[Int] = n def add(e1: Repr[Int], e2: Repr[Int]): Repr[Int] = e1 + e2 def lambda[A, B](f: A => B): A => B = f def apply[A, B](f: A => B, x: A): B = f(x) }
  28. ドメインロジックのなかでクエリがしたい 期首の為替レートで計算した明細の一覧がほしい 91 for record in Transactions: yield ( {

    department: record.department, amount: if record.currency != "JPY" then record.amount * ExchangeRate[record.currency][FiscalPeriod.startMonth] else record.amount, scenario: CurrentScenario } )
  29. Layered Final 96 • tagless-finalをベースにしつつ、多層のインタプリタ(意味)を与え最適化を施せるように したアプローチ • Ad-hoc Rewriteだと拡張や保守が困難であるため、管理しやすいようにパイプライン化 DSL

    Frontend (Symantics) Expression Lifting Fusion Algebraic Normalizer Composition Engine Code Generator Execution Backend (Interpreter) classic module classic module Fusion IR Any Bridge e.g.) Writer, Provenance
  30. Symantics for内包表記に相当するモナディックな操作 97 trait QuerySym: type Repr[+_] def table[A](name: String):

    Repr[List[A]] def map[A, B](src: Repr[A], f: A => B): Repr[B] def flatMap[A, B](src: Repr[A], f: A => Repr[B]): Repr[B] def filter[A](src: Repr[A], p: A => Boolean): Repr[A] def union[A](left: Repr[A], right: Repr[A]): Repr[A] def empty[A]: Repr[A] (以下略)
  31. Fusion的中間表現 GADT(一般化代数的データ型)を使って、DSLの項の型安全性を守る 98 enum Fusion[+A]: case Yield(value: A) case Map[S,

    B](source: Fusion[S], f: S => B) extends Fusion[B] case FlatMap[S, B](source: Fusion[S], f: S => Fusion[B]) extends Fusion[B] case Filter[S](source: Fusion[S], p: S => Boolean) extends Fusion[S] case Union(left: Fusion[A], right: Fusion[A]) extends Fusion[A] case Table[A](name: String) extends Fusion[List[A]] case Empty
  32. Fusion的中間表現 IRに変換するインタプリタ 99 object ExpressionLifter extends QuerySym: type Repr[+A] =

    Fusion[A] def table[A](name: String): Fusion[List[A]] = Fusion.Table[A](name) def map[A, B](src: Fusion[A], f: A => B): Fusion[B] = Fusion.Map(src, f) def flatMap[A, B](src: Fusion[A], f: A => Fusion[B]): Fusion[B] = Fusion.FlatMap(src, f) def filter[A](src: Fusion[A], p: A => Boolean): Fusion[A] = Fusion.Filter(src, p) def union[A](l: Fusion[A], r: Fusion[A]): Fusion[A] = Fusion.Union(l, r) def empty[A]: Fusion[A] = Fusion.Empty
  33. Fusion Normalizer 代数的性質として正規化する (正規化規則を精密に組み合わせるの結構大変なので) 100 操作 代数法則 (Rewrite Law) Map

    Fusion map(map(xs, f), g) ⇒ map(xs, g ∘ f) Filter Fusion filter(filter(xs, p1), p2) ⇒ filter(xs, p1 ∧ p2) Union Identity union(xs, empty) ⇒ xs FlatMap Stability flatMap(xs, f) ⇒ flatMap(normalize(xs), f)
  34. Fusion Normalizer 正規化もインタプリタとして構成する 101 class NormalizerInterpreter(base: QuerySym { type Repr[+X]

    = Fusion[X] }) extends QuerySym: type Repr[+A] = Fusion[A] def table[A](name: String): Fusion[List[A]] = normalize(base.table(name)) def map[A, B](src: Fusion[A], f: A => B): Fusion[B] = normalize(base.map(src, f)) def flatMap[A, B](src: Fusion[A], f: A => Fusion[B]): Fusion[B] = normalize(base.flatMap(src, f)) def filter[A](src: Fusion[A], p: A => Boolean): Fusion[A] = normalize(base.filter(src, p)) def union[A](l: Fusion[A], r: Fusion[A]): Fusion[A] = normalize(base.union(l, r)) def empty[A]: Fusion[A] = normalize(base.empty) (以下略)
  35. taglessのインタプリタはそれぞれが別のReprを持っている 102 trait QuerySym: type Repr[+_] def table[A](name: String): Repr[List[A]]

    def map[A, B](src: Repr[A], f: A => B): Repr[B] def flatMap[A, B](src: Repr[A], f: A => Repr[B]): Repr[B] def filter[A](src: Repr[A], p: A => Boolean): Repr[A] def union[A](left: Repr[A], right: Repr[A]): Repr[A] def empty[A]: Repr[A] object EvalInterpreter extends QuerySym: type Repr[+A] = () => List[A] def table[A](name: String): () => List[List[A]] = () => List(db.getOrElse(name, Nil).asInstanceOf[List[A]]) def map[A, B](src: () => List[A], f: A => B): () => List[B] = () => src().map(f) (以下略) class WriterInterpreter(   base: QuerySym { type Repr[+X] = Fusion[X] }) extends QuerySym: type Repr[+A] = WriterFusion[A] private inline def add(msg: String, xs: List[String]) = xs :+ msg def table[A](name: String): WriterFusion[List[A]] = WriterFusion(base.table(name), List(s"table($name)")) def map[A, B](src: WriterFusion[A], f: A => B): WriterFusion[B] = WriterFusion(base.map(src.ir, f), add("map", src.log)) (以下略) type Repr[+A] = () => List[A] type Repr[+A] = WriterFusion[A]
  36. 層を繋ぐ 103 trait LayerBridge[F[+_], G[+_]]: def embed[A](fa: F[A]): G[A] def

    project[A](ga: G[A]): F[A] いわゆる自然変換 (natural transformation) embed project F の世界 G の世界 A A Embedding-Projection Pair構造 各層が局所的に変換を定義するだ けで積層可能に 変換 変換
  37. 順序付き積層: 層ごとの世界を繋いで、インタプリタを積み上げる 層ごとの変換を安全に合成して、柔軟にパイプライン全体を組み立てる 105 class LayerStack[Base[+_], Current[+_]]( val bridge: LayerBridge[Base,

    Current] ): def addLayer[Next[+_]](next: LayerBridge[Current, Next]): LayerStack[Base, Next] = new LayerStack(LayerBridge.compose(bridge, next)) def build(baseInterpreter: QuerySym { type Repr[+A] = Base[A] }): QuerySym = new QuerySym: type Repr[+A] = Current[A] def table[A](name: String): Repr[List[A]] = bridge.embed(baseInterpreter.table(name)) def map[A, B](src: Repr[A], f: A => B): Repr[B] = bridge.embed(baseInterpreter.map(bridge.project(src), f)) (以下略) 世界を渡る インタプリタを生成
  38. 順序付き積層: 層ごとの世界を繋いで、インタプリタを積み上げる 層ごとの変換を安全に合成して、柔軟にパイプライン全体を組み立てる 106 class LayerStack[Base[+_], Current[+_]]( val bridge: LayerBridge[Base,

    Current] ): def addLayer[Next[+_]](next: LayerBridge[Current, Next]): LayerStack[Base, Next] = new LayerStack(LayerBridge.compose(bridge, next)) def build(baseInterpreter: QuerySym { type Repr[+A] = Base[A] }): QuerySym = new QuerySym: type Repr[+A] = Current[A] def table[A](name: String): Repr[List[A]] = bridge.embed(baseInterpreter.table(name)) def map[A, B](src: Repr[A], f: A => B): Repr[B] = bridge.embed(baseInterpreter.map(bridge.project(src), f)) (以下略) G へのembed F へのproject
  39. Code Generator, Evaluator 例えばSQLを生成する 109 object FusionToSQL: private var aliasCounter

    = 0 private def nextAlias(): String = aliasCounter += 1 s"t$aliasCounter" def toSQL[A](ir: Fusion[A]): String = ir match case Fusion.Table(name) => s"SELECT * FROM $name" case Fusion.Map(src, f) => val alias = nextAlias() val srcQuery = toSQL(src) s"SELECT ${mapFunctionToSQL(f)} FROM ($srcQuery) AS $alias"    (以下、省略)
  40. 可変の差別化要素 ソフトウェアプロダクトライン(Software Product Line; SPL) 113 認証認可 API基盤 メッセージング・ 通知

    ロギング・監査 … 共通基本構造・コンポーネント 課金ルール 定額/従量/階段式/ 複合課金 配賦・按分ルール 配賦基準/多段階配賦/ 配賦対象/しきい値 承認フロー 予算承認/期中修正ルール/ 金額閾値分岐 … ワークフロー エンジン レポート エンジン 配賦・仕訳生成 ルールエンジン 料金計算 ロジック 給与計算 エンジン クエリエンジン キャッシュ層 マルチテナント 抽象レイヤ 権限制御 IP制限 集計/スナップ ショット 認証認可 … ドメイン基盤 技術基盤 シナリオ・シミュレーション ドライバー定義/シナリオ分岐 /変動係数 共通の製造手段を使用し、共有されたソフトウェア資産から類似のソフトウェアシステムのコレクションを 作成するための手法
  41. 似て非なるユースケース差分 • 汎用エンジンに寄せる → 重厚長大化 • 個別・個社開発に寄せる → 保守難易度高 116

    可変ポイントの形式化 ドメインエキスパート+AIによる開発 ガバナンスレイヤー 共通資産レイヤー 可変ポイントレイヤー DSLエキスパート ドメインエキスパート AIエージェント 基盤エンジニア 安全なコードしか書けない AIエージェントが生成してもある程 度の信頼性は担保
  42. まとめ 119 • 曖昧さが偶有的複雑性の温床になる • tagless-finalアプローチはメタ言語の力を借りて型安全にDSLを作れる • tagless-finalで作った言語は抽象の特性を持たせやすい ◦ 安定度・抽象度等価の原則

    ◦ 安定依存の原則 • インタプリタを後付けできるので ◦ 早期のビジネス検証に最適 ◦ あとから最適化しやすい • Layered Final で実用性の高い最適化諸々パイプラインを構築 • AI駆動&SPLでtagless-final DSLを導入することで、 開発の出力を最大化