Slide 1

Slide 1 text

ドメインモデリングにおける抽象の役割、 tagless-finalによるDSL構築、 そして型安全な最適化 knih (Kenichi Suzuki) 関数型まつり2025

Slide 2

Slide 2 text

Cotents ● 導入 ● 複雑性との戦いと抽象の役割 ● tagless-finalによるDSL構築:抽象の具現化 ● クエリDSLと最適化: Layered Final ● DSLの活用:AI × ソフトウェアプロダクトライン ● まとめ 2

Slide 3

Slide 3 text

自己紹介    Kenichi Suzuki @_knih QUEΛ: https://bitbucket.org/knih/quel/ 3 Scala秋まつり ScalaMatsuri 2020 PEPM2016, PPL2015, ... 他いろいろ

Slide 4

Slide 4 text

ビジネスの本質をコードにどう表現していますか? 4

Slide 5

Slide 5 text

崩壊するモデル 5

Slide 6

Slide 6 text

もともとはキレイなドメインモデル ● 明確な責務 ● 予測可能な状態遷移 ● 理解とメンテナンスが容易 case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit } ビジネスモデルを明確に反映して信頼性が高い状態

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

モデルの外部に判断が漏れる 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 そして、構造がルールを定義しなく なると、意味は失われ始める

Slide 9

Slide 9 text

意味を保持できなくなったモデル 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()

Slide 10

Slide 10 text

使う側に意味の判断を委ねると 意味を喪失しやすくなる 10

Slide 11

Slide 11 text

そこでDSL 11

Slide 12

Slide 12 text

DSL(ドメイン特化言語)とは 12 ● 特定のドメインにおける問題解決を目的として設計された言語 ● 汎用プログラミング言語とは異なり、そのドメインの概念や用語、規則を直 接表現できる ● ドメインの意味を直接表現可能にする「言語的構造」を与える ○ 誤った意味を記述できないようにしてしまえばいい DSLは「意味を埋め込む型安全なインターフェース」とも言える 後述するTagless-Finalはその一つの実装技法

Slide 13

Slide 13 text

抽象の特性をコードで具現化する:DSLの力 13 DSL ドメインの言葉を使って ドメインを記述できる DSLは、ドメインの言葉で直接問題を記述することで、ビジネスの「本質」をコー ドに正確に映し出し、偶有的複雑性を排除 顧客管理 コンテキスト 在庫管理 コンテキスト 受注コンテキスト ドメインモデル DSL

Slide 14

Slide 14 text

意味の曖昧さは偶有的複雑性の起点になる 14 ● モデルを言語として定式化することで、どの操作が許され、どの解釈が正当 かを型レベル・構文レベルで制約できる ● 逆に言語で表現しないまま自由に解釈させると、各実装者が都度判断し、設 計方針がばらける 解釈のばらつきは偶有的複雑性の蓄積のはじまり DSLの構築は複雑性のケアが重要

Slide 15

Slide 15 text

複雑性との戦い 15

Slide 16

Slide 16 text

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.

Slide 17

Slide 17 text

すべてのソフトウェア構築には、 本質的作業として抽象的なソフトウェア 実体エンティティを構成する複雑な概念構造体を作り上げること、 およ び、 偶有的作業としてそうした抽象的実在をプログラミング言語で表現 し、 それをメモリスペースとスピードの制約内で機械言語に写像するこ とが含まれている。 Frederick P. Brooks, JR. 著, 滝沢徹, 牧野祐子, 富澤昇 訳. 『人月の神話』, 第16章. 丸善出版 (2014) “ 銀の弾丸はない ー ソフトウェアエンジニアリングの本質と偶有的事項 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

複雑性との向き合い方 複雑性の性質にあわせた向き合い方をする 19 本質的複雑性 偶有的複雑性 いかに共存し、理解し、価値を引き出すか いかに排除し、削減し、発生源を根絶するか 分離する

Slide 20

Slide 20 text

抽象の役割 20

Slide 21

Slide 21 text

抽象 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.

Slide 22

Slide 22 text

抽象の本質 22 ドメインの意図を崩さずに、本質を抽出し、その意図を実装可能な形で表現する 課題の解像度を高め、予測可能性を高めること               (=安定的に成功を再現させる) 複雑で曖昧な現実から 「本質的で再現可能なパターン」を見つけ出す 問題空間は常に変化し、複雑で不安定 解決空間においては、安定性や再現性(予測 可能性)が求められる

Slide 23

Slide 23 text

抽象の役割 23 在庫管理 ドメイン 受注ドメイン 顧客管理 ドメイン 顧客管理 コンテキスト 在庫管理 コンテキスト 受注コンテキスト 問題空間 解決空間 現実世界 ドメインモデル 抽象 問題空間と解決空間の橋渡しをする

Slide 24

Slide 24 text

安定依存の原則と安定度・抽象度等価の原則 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. 依存方向

Slide 25

Slide 25 text

入念に設計された抽象の特性 25

Slide 26

Slide 26 text

抽象の特性 26 最小化 蒸留 合成可能性 拡張可能性 表現力 テスト容易性 直交性 変更容易性

Slide 27

Slide 27 text

抽象の特性:本質の抽出 (意味の核を形成する) 27 最小化 Minimalism 蒸留 Distillation 表現力 Expressiveness 意味の核以外のノイズを除去する ドメイン知識から本質概念を抽出し、DSLの語彙として定義する 必要なものだけを公開し、余計なものを隠す DSLに含める演算・型・操作を厳選。誤用を減らし、意味の純度を保つ 意味が明快に表現されており、意図が伝わる ドメインの意図が直接・簡潔にDSLコードに表現される

Slide 28

Slide 28 text

抽象の特性:構造に作用する 28 合成可能性 Composability 拡張可能性 Extensibility 直交性 Orthogonality 合成可能な構造であること。意味が壊れずに繋がる DSLの操作が安全に合成可能であること(例:式のネスト、関数合成) 他の構成要素と独立に機能し、影響を与え合わない DSL内の各構成要素が独立しており、干渉しにくい 将来的な拡張に強く、壊れにくい 将来の新しいビジネス要件に対応する拡張ポイントを設計する

Slide 29

Slide 29 text

抽象の特性:変更や検証のしやすさ 29 テスト容易性 Testability 変更容易性 Modifiability 要件変更に際して壊れにくく、適応が容易 DSLの変更が他の部分に影響を及ぼしにくく設計されている 単体で検証可能で、テストしやすい構造になっている DSLインタフェース単位で検証しやすい(PBTも適用しやすい)

Slide 30

Slide 30 text

蒸留:必要なもののみ残す 30 ドメインの中から意味の核を抽出し、余計な詳細(nonessential detail)を取り除く 何が余計か? 設計の詳細が、その抽象の中核となる関心事に対応付けられなかった場合、 それは余計である DSL設計の第一歩 は蒸留から

Slide 31

Slide 31 text

余計な詳細の例:偶有的複雑性の一例 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) } } // ... }

Slide 32

Slide 32 text

余計な詳細の例:偶有的複雑性の一例 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) } } // ... } 余計な詳細 偶有的な複雑さ

Slide 33

Slide 33 text

余計な詳細の例: ノイズを除去する 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パターン というやつ

Slide 34

Slide 34 text

本物のtagless-finalはこんなもんじゃない 34

Slide 35

Slide 35 text

tagless-finalによるDSL構築: 抽象の具現化 reification of abstraction 35

Slide 36

Slide 36 text

● 型安全な埋め込み言語を構築する手法(≠tagless-fnalパターン) ● DSLの型付けがホストとなる言語のそれに帰着する ○ 型検査アルゴリズムを実装しなくてよい ● HOASが使える ○ ホストとなる言語によってスコープ安全性が担保される ● 言語コンポーネントの合成が可能 ● パーサーを作らなくて良い ● ホスト言語のエコシステムが使える(IDE、AIエージェント等々) tagless-finalとは 36 Claude Codeでtagless プログラムを書くのがアツい!

Slide 37

Slide 37 text

メタ言語 (meta language) 対象言語とメタ(ホスト)言語 37 対象言語 (object language) 表現したい言語、DSL DSLの埋め込み先となる言語 e.g.) Scala, Rust 構文はメタ言語の関数機構、型付けはメタ言語の型システムの帰着

Slide 38

Slide 38 text

言語を構成する三要素:構文、型付け規則、意味論 38 インタフェース インタプリタ インタプリタ インタプリタ 構文&型付け規則 意味論 複数の意味をもた せることができる Symantics: Syntax, Typing rules and Semantics

Slide 39

Slide 39 text

インタフェースを書いてその実装をつくれば tagless-finalってこと・・・? 39

Slide 40

Slide 40 text

No 大事なのはここから 40

Slide 41

Slide 41 text

言語を設計する 41

Slide 42

Slide 42 text

対象言語を設計する 42 Syntax

Slide 43

Slide 43 text

対象言語を設計する: Syntax 43 Syntax 例:

Slide 44

Slide 44 text

対象言語を設計する: Syntax 44 Syntax 例:

Slide 45

Slide 45 text

対象言語を設計する: Syntax 45 Syntax 例:

Slide 46

Slide 46 text

対象言語を設計する: Syntax 46 Syntax 例:

Slide 47

Slide 47 text

対象言語を設計する: Syntax 47 Syntax 例:

Slide 48

Slide 48 text

対象言語を設計する: Typing Rules 48 Syntax Typing Rules

Slide 49

Slide 49 text

対象言語を設計する: Typing Rules 49 Syntax Typing Rules 上が成立していれば 下を導くことができる

Slide 50

Slide 50 text

対象言語を設計する: Typing Rules 50 Syntax Typing Rules 上が成立していれば 下を導くことができる n が整数であるならば n は整数(Int) の型を持つ

Slide 51

Slide 51 text

対象言語を設計する: Typing Rules 51 Syntax Typing Rules 上が成立していれば 下を導くことができる n が整数であるならば n は整数(Int) の型を持つ e1 が整数であるならば かつ e2 が整数であるならば e1 + e2 は整数(Int) の型を持つ

Slide 52

Slide 52 text

対象言語を設計する: Semantics 52 Syntax Typing Rules Semantics

Slide 53

Slide 53 text

対象言語を設計する: Semantics 53 n は n と評価される Syntax Typing Rules Semantics

Slide 54

Slide 54 text

対象言語を設計する: Semantics 54 n は n と評価される e1 は n1 に評価される Syntax Typing Rules Semantics

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

対象言語を設計する 58 Syntax Typing Rules Semantics

Slide 59

Slide 59 text

tagless-final エンコーディング 59 Classical

Slide 60

Slide 60 text

世界を分かち、繋ぐ 60 Point

Slide 61

Slide 61 text

世界を分かつ 61 言語の世界をメタ言語(ホスト言語)と対象言語とに分けて考える メタ言語 (meta language) 対象言語 (object language)

Slide 62

Slide 62 text

tagless-finalの エンコーディング手順 62

Slide 63

Slide 63 text

エンコーディング手順 63 Syntax Typing Rules

Slide 64

Slide 64 text

エンコーディング手順:まずは構文を関数で表現 64 Syntax Typing Rules trait ArithSym { def int(n: Int): Int def add(e1: Int, e2: Int): Int }

Slide 65

Slide 65 text

エンコーディング手順:まずは構文を関数で表現 65 Syntax Typing Rules trait ArithSym { def int(n: Int): Int def add(e1: Int, e2: Int): Int } このままでは 世界がごっちゃ になる

Slide 66

Slide 66 text

意味の曖昧さは偶有的複雑性の起点になる (大事なことなのでもう一度) 66

Slide 67

Slide 67 text

エンコーディング手順:私たちの世界の型を表現する 67 Syntax Typing Rules trait ArithSym { def int(n: Int): Int def add(e1: Int, e2: Int): Int }

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

エンコーディング手順:私たちの世界の型を表現する 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で包む

Slide 70

Slide 70 text

エンコーディング手順:私たちの世界の型を表現する 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で包む

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

世界を型で分かつ 72 言語の世界をメタ言語(ホスト言語)と対象言語とに分けて考える メタ言語 (meta language) 対象言語 (object language) Int Repr[Int]

Slide 73

Slide 73 text

注意: Polymorphic Lifting メタ言語から対象言語の世界への流入経路は慎重に設計する 多相の値を受け取ると、意図しない値が世界に紛れ込む 73 def int(n: Int): Repr[Int] def lit[A](n: A): Repr[A] 非推奨 OK 世界の入口 世界の入口

Slide 74

Slide 74 text

この時点で構文・型が完成 ユーザープログラムを記述可能に 74

Slide 75

Slide 75 text

ユーザープログラムを書く 75

Slide 76

Slide 76 text

ユーザープログラム 変数機構はホスト言語のものがそのまま使える 76 class Example[S <: ArithSym](val sym: S): import sym._ def example1: Repr[Int] = add(int(1), int(2))

Slide 77

Slide 77 text

ユーザープログラム 変数機構はホスト言語のものがそのまま使える 77 class Example[S <: ArithSym](val sym: S): import sym._ def example1: Repr[Int] = add(int(1), int(2)) Reprの世界になっている

Slide 78

Slide 78 text

ドメインの記述でtagless-finalの嬉しいところ 78 扱いたいビジネスドメインを表現できるかどうか 早期に検証できる 偶有的複雑性を排除しながら ユーザープログラムを記述できる

Slide 79

Slide 79 text

インタプリタ(意味論)をつくる 79

Slide 80

Slide 80 text

意味論の実装:設計をなぞるだけ 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 と置く

Slide 81

Slide 81 text

意味論の実装:複数のインタプリタ 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として扱う インタプリタを切り替えても、ユーザープログラムには影響しない

Slide 82

Slide 82 text

実行する 82

Slide 83

Slide 83 text

ユーザープログラムを実行する 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) } ユーザープログラムは同じ インタプリタ インタプリタ

Slide 84

Slide 84 text

言語を拡張する、合成する 84

Slide 85

Slide 85 text

言語を合成できる、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) }

Slide 86

Slide 86 text

クエリDSLと最適化: Layered Final 86

Slide 87

Slide 87 text

みんな大好きクエリ言語 87 ● 単なるデータではなく、意味をもったドメインの計算対象をクエリしたい ユーザーの問い 背景 来期の粗利は? 計算式、期間ロジック、為替、予算比較 このプロジェクトのROIは? 原価按分、投資配賦、シナリオ前提 為替影響を除いた実質成長 率は? 時系列補正、基準変更

Slide 88

Slide 88 text

みんな大好きクエリ言語 88 ● Nested Relational Calculus(NRC)ベース ○ Bag(多重集合)を扱うリレーショナル計算体系

Slide 89

Slide 89 text

みんな大好きクエリ言語 89 ● Nested Relational Calculus(NRC)ベース ○ Bag(多重集合)を扱うリレーショナル計算体系 for x in Product where x.price > 10000 yield x

Slide 90

Slide 90 text

ドメインロジックのなかでクエリがしたい 期首の為替レートで計算した明細の一覧がほしい 90 for record in Transactions yield(record.amount * ExchangeRate[record.currency][FiscalPeriod.startMonth])

Slide 91

Slide 91 text

ドメインロジックのなかでクエリがしたい 期首の為替レートで計算した明細の一覧がほしい 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 } )

Slide 92

Slide 92 text

特に、更新系ではなく参照系(+計算)が多いプロダクトでは、 クエリそれ自体がドメイン資産になる (が、SQLだとインフラ層に押し込められて再利用性が低い) 92

Slide 93

Slide 93 text

とはいえ、クエリにパフォーマンス問題は付きもの 93

Slide 94

Slide 94 text

DSL上のクエリを最適化するには? 94

Slide 95

Slide 95 text

Layered Final 95 と名付けてみました

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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] (以下略)

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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)

Slide 101

Slide 101 text

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) (以下略)

Slide 102

Slide 102 text

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]

Slide 103

Slide 103 text

層を繋ぐ 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構造 各層が局所的に変換を定義するだ けで積層可能に 変換 変換

Slide 104

Slide 104 text

104 インタプリタのミルフィーユをつくる

Slide 105

Slide 105 text

順序付き積層: 層ごとの世界を繋いで、インタプリタを積み上げる 層ごとの変換を安全に合成して、柔軟にパイプライン全体を組み立てる 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)) (以下略) 世界を渡る インタプリタを生成

Slide 106

Slide 106 text

順序付き積層: 層ごとの世界を繋いで、インタプリタを積み上げる 層ごとの変換を安全に合成して、柔軟にパイプライン全体を組み立てる 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

Slide 107

Slide 107 text

順序付き積層: 層ごとの世界を繋いで、インタプリタを積み上げる 層ごとの変換を安全に合成して、柔軟にパイプライン全体を組み立てる 107 object FusionLayerStack: val stack = LayerStack[Fusion, Fusion](LayerBridge.identity[Fusion]) .addLayer(NormalizerBridge) .addLayer(WriterBridge)

Slide 108

Slide 108 text

インタプリタのミルフィーユ イメージです 108 ミルフィーユっぽい! おれたちの インタプリタ 世界を渡る インタプリタ おれたちの インタプリタ

Slide 109

Slide 109 text

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"    (以下、省略)

Slide 110

Slide 110 text

Layered Final ● クエリ言語のような最適化や変換、あるいはトレーシングのためのロギング やProvenanceを後付けしたいときに便利 ● 層を追加しても削除しても、DSLそれ自体やユーザープログラムに影響は 無い ● ユーザープログラムやDSL自体を早期にビジネス検証できる旨味は健在 ● 実用面ではアプリエンジニアというよりは、アーキテクトや基盤エンジニアが 担当すると良さそう 110

Slide 111

Slide 111 text

DSLで開発の出力を上げる: AI × ソフトウェアプロダクトライン 111

Slide 112

Slide 112 text

可変の差別化要素 ソフトウェアプロダクトライン(Software Product Line; SPL) 112 共通の製造手段を使用し、共有されたソフトウェア資産から類似のソフトウェアシステムのコレクションを 作成するための手法 液晶パネル 電源 OS 基本UI … 共通基本構造・コンポーネント サイズ (32型〜75型) 画質 (フルHD、4K、8K) 国ごとの仕様 (電圧、放送方式、言語対応) …

Slide 113

Slide 113 text

可変の差別化要素 ソフトウェアプロダクトライン(Software Product Line; SPL) 113 認証認可 API基盤 メッセージング・ 通知 ロギング・監査 … 共通基本構造・コンポーネント 課金ルール 定額/従量/階段式/ 複合課金 配賦・按分ルール 配賦基準/多段階配賦/ 配賦対象/しきい値 承認フロー 予算承認/期中修正ルール/ 金額閾値分岐 … ワークフロー エンジン レポート エンジン 配賦・仕訳生成 ルールエンジン 料金計算 ロジック 給与計算 エンジン クエリエンジン キャッシュ層 マルチテナント 抽象レイヤ 権限制御 IP制限 集計/スナップ ショット 認証認可 … ドメイン基盤 技術基盤 シナリオ・シミュレーション ドライバー定義/シナリオ分岐 /変動係数 共通の製造手段を使用し、共有されたソフトウェア資産から類似のソフトウェアシステムのコレクションを 作成するための手法

Slide 114

Slide 114 text

エンタープライズプロダクトの価値 114 プロダクト のコア価値

Slide 115

Slide 115 text

エンタープライズプロダクトの価値 115 A社にとっての 価値 B社にとって の価値 D社にとって の価値 C社にとって の価値 プロダクト のコア価値 ※事業戦略・プロダクト戦略によります

Slide 116

Slide 116 text

似て非なるユースケース差分 ● 汎用エンジンに寄せる → 重厚長大化 ● 個別・個社開発に寄せる → 保守難易度高 116 可変ポイントの形式化 ドメインエキスパート+AIによる開発 ガバナンスレイヤー 共通資産レイヤー 可変ポイントレイヤー DSLエキスパート ドメインエキスパート AIエージェント 基盤エンジニア 安全なコードしか書けない AIエージェントが生成してもある程 度の信頼性は担保

Slide 117

Slide 117 text

AI時代に向けた開発 AI+SPL+DSLで型安全に 広範囲な業務ユースケースをカバー 117

Slide 118

Slide 118 text

まとめ 118

Slide 119

Slide 119 text

まとめ 119 ● 曖昧さが偶有的複雑性の温床になる ● tagless-finalアプローチはメタ言語の力を借りて型安全にDSLを作れる ● tagless-finalで作った言語は抽象の特性を持たせやすい ○ 安定度・抽象度等価の原則 ○ 安定依存の原則 ● インタプリタを後付けできるので ○ 早期のビジネス検証に最適 ○ あとから最適化しやすい ● Layered Final で実用性の高い最適化諸々パイプラインを構築 ● AI駆動&SPLでtagless-final DSLを導入することで、 開発の出力を最大化

Slide 120

Slide 120 text

EOF 120