Slide 1

Slide 1 text

© 2024 Loglass Inc. 1 © 2024 Loglass Inc. Designing for Contextual Integrity From Domain Models To Domain Languages with Tagless-Final Kenichi Suzuki Loglass Inc.

Slide 2

Slide 2 text

© 2024 Loglass Inc. 2 About Me Logass Inc. General Manager, Enabling & Platform Senior Engineering Manager Kenichi SUZUKI X: @_knih Having built a strong foundation as an Architect in mission-critical, large-scale systems at NTT Data, I pursued research in programming language theory to achieve more secure software development. Drawing upon this expertise, I spearheaded the launch, product management, and development of the cybersecurity business at Visional (BizReach), thereby contributing to a safer world by delivering new market value. Subsequently, I held key leadership roles at ContractS, including Head of Development, Head of Product, Head of Technology Strategy Office, and VP of Development, before joining Loglass in 2023.

Slide 3

Slide 3 text

© 2024 Loglass Inc. 3 ✔ Understand why domain models fall apart over time ✔ Learn why it matters to treat a model as a language ✔ Discover how to design domains as languages using Tagless-Final ✔ See the principles of syntax, typing, and semantics in action ✔ Explore the concept that domains are actually algebras Today’s Goal

Slide 4

Slide 4 text

© 2024 Loglass Inc. 4 Outline 01 | The Crumbling Domain Model 02 | Model as Language 03 | The Tagless-Final Approach 04 | Applying the Language: Fiscal Periods 05 | Advanced Topics: Algebras and Complexity 06 | Summary

Slide 5

Slide 5 text

© 2024 Loglass Inc. 5 01 The Crumbling Domain Model

Slide 6

Slide 6 text

© 2024 Loglass Inc. 6 Why Do Models Decay?

Slide 7

Slide 7 text

© 2024 Loglass Inc. 7 01 | Crumbling Domain Model Once, the Domain Model Was Beautiful ● Clear responsibilities ● Predictable transitions ● Easy to understand and maintain case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit } The model reflected business concepts clearly, and everyone trusted it.

Slide 8

Slide 8 text

© 2024 Loglass Inc. 8 01 | Crumbling Domain Model Then, Change Came case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } It was the right thing to do. We want to temporarily close the period for a preliminary quarter-end report. We’d rather not fully close it until the audit is signed off. Let’s lock it for now—we’re still waiting on reports from other departments.

Slide 9

Slide 9 text

© 2024 Loglass Inc. 9 01 | Crumbling Domain Model Judgment Leaked Outside the Model case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } temporaryClose seems safer. It depends on the situation... I’ll just use close(). Uses temporaryClose() Uses both with if logic Ignores temporaryClose() Dev A Dev B Dev C And when the structure stops defining the rules, meaning begins to erode.

Slide 10

Slide 10 text

© 2024 Loglass Inc. 10 01 | Crumbling Domain Model The Structure Could No Longer Hold the Meaning case class FiscalPeriod( unit: FiscalPeriodUnit, year: Int, periodNumber: Int, var isOpen: Boolean,...) { def open(): Unit def close(): Unit def temporaryClose(): Unit } Uses temporaryClose() Uses both with if logic Ignores temporaryClose() Dev A Dev B Dev C The vocabulary remained… But the structure no longer protected it. if (isPreliminary) period.temporaryClose() if (isAudited) period.close() else period.temporaryClose() period.close()

Slide 11

Slide 11 text

© 2024 Loglass Inc. 11 02 Model as Language Preserving meaning through structural expression

Slide 12

Slide 12 text

© 2024 Loglass Inc. 12 02 | Model as Language Model as Language Meaning breaks when language lacks structure. “Yes” “temporaryClose()” Yes I see reviewer? accounting? Natural Language Domain Model

Slide 13

Slide 13 text

© 2024 Loglass Inc. 13 02 | Model as Language What Makes a Language Well-Defined? Well-defined languages are sound: They accept only valid expressions and reject invalid ones. Syntax Types Semantics What does it mean? What is valid? What can be written? A well-defined language lets you say only what makes sense.

Slide 14

Slide 14 text

© 2024 Loglass Inc. 14 02 | Model as Language Arithmetic Expression Language Syntax Types Semantics 1 + (2 - 3) ⇒ 1 - 1 ⇒ 0 Int 1 + (2 - 3) Well-defined languages preserve meaning through structure. As An Example

Slide 15

Slide 15 text

© 2024 Loglass Inc. 15 02 | Model as Language Arithmetic Expression Language Syntax Types Semantics (T-Lit) (T-Add) (T-Sub) Formal Definition [[ ]] is meaning function

Slide 16

Slide 16 text

© 2024 Loglass Inc. 16 03 The Tagless-Final Approach

Slide 17

Slide 17 text

© 2024 Loglass Inc. 17 03 | The Tagless-Final Approach From Definition to Implementation: The Tagless-Final Approach Syntax Types Semantics Well-defined language Naive implementation ● Hard-coded evaluation logic ● No structure in code ● Cannot ensure type safety ● Meaning and usage become entangled A safe language is only meaningful if it’s safely implemented.

Slide 18

Slide 18 text

© 2024 Loglass Inc. 18 03 | The Tagless-Final Approach What Is the Tagless-Final Approach? Tagless-Final is a method of embedding a language in a host language. ● Terms of the object language are written as host-language terms ● Typing and semantics are expressed in terms of the host language ● Typing of the embedded language is reduced to typing in the host language ● No external type checker or evaluator is needed Tagless-Final was named by Oleg Kiselyov, a pioneer in functional programming theory.

Slide 19

Slide 19 text

© 2024 Loglass Inc. 19 03 | The Tagless-Final Approach Techniques behind Tagless-Final ● HOAS (Higher-Order Abstract Syntax) ○ Represent object-language syntax using functions in the host language ● Final encoding ○ Define syntax as an interface, not a data type ● Parametric polymorphism ○ Delay interpretation by abstracting over meaning ● Type class–based interpretation ○ Switch between multiple semantics in a modular way Tagless-Final unifies key techniques from functional programming: ● Host-typed safety ○ Ensure well-typed object programs without needing a separate type checker ● Modular language construction ○ Compose and extend language fragments as traits or type classes ● Specialization and staging ○ Enable partial evaluation and code generation à la Futamura

Slide 20

Slide 20 text

© 2024 Loglass Inc. 20 03 | The Tagless-Final Approach Object Language vs. Host Language In Tagless-Final, we define the object language within the host language. Object Language Host Language ● Scala ● Rust ● Haskell ● OCaml … ● Arithmetic expressions ● Business rules … ⊂ The host language provides the type system and abstraction power. The object language provides the domain logic.

Slide 21

Slide 21 text

© 2024 Loglass Inc. 21 Interpreters defines Semantics Interpreters defines Semantics 03 | The Tagless-Final Approach Symantics: From Interface to Interpretation In Tagless-Final, syntax and types are encoded in the interface, and semantics are defined by implementing that interface. Interface encodes Syntax & Types Semantics Interpreters defines Semantics Syntax & Types Symantics Each interpreter gives semantics to the same interface.

Slide 22

Slide 22 text

© 2024 Loglass Inc. 22 03 | The Tagless-Final Approach Encoding Syntax and Types as an Interface Syntax Types (T-Lit) (T-Add) (T-Sub) trait ArithExprSym { type Repr[T] def lit(n: Int): Repr[Int] def add(x: Repr[Int], y: Repr[Int]): Repr[Int] def sub(x: Repr[Int], y: Repr[Int]): Repr[Int] } This interface encodes both syntax and types in a single type-safe structure.

Slide 23

Slide 23 text

© 2024 Loglass Inc. 23 03 | The Tagless-Final Approach Encoding Syntax and Types as an Interface Syntax Types (T-Lit) (T-Add) (T-Sub) trait ArithExprSym { type Repr[T] def lit(n: Int): Repr[Int] def add(x: Repr[Int], y: Repr[Int]): Repr[Int] def sub(x: Repr[Int], y: Repr[Int]): Repr[Int] } This interface encodes both syntax and types in a single type-safe structure.

Slide 24

Slide 24 text

© 2024 Loglass Inc. 24 03 | The Tagless-Final Approach Encoding Syntax and Types as an Interface Syntax Types (T-Lit) (T-Add) (T-Sub) trait ArithExprSym { type Repr[T] def lit(n: Int): Repr[Int] def add(x: Repr[Int], y: Repr[Int]): Repr[Int] def sub(x: Repr[Int], y: Repr[Int]): Repr[Int] } This interface encodes both syntax and types in a single type-safe structure.

Slide 25

Slide 25 text

© 2024 Loglass Inc. 25 03 | The Tagless-Final Approach Encoding Syntax and Types as an Interface Syntax Types (T-Lit) (T-Add) (T-Sub) trait ArithExprSym { type Repr[T] def lit(n: Int): Repr[Int] def add(x: Repr[Int], y: Repr[Int]): Repr[Int] def sub(x: Repr[Int], y: Repr[Int]): Repr[Int] } This interface encodes both syntax and types in a single type-safe structure.

Slide 26

Slide 26 text

© 2024 Loglass Inc. 26 03 | The Tagless-Final Approach Encoding Syntax and Types as an Interface Syntax Types (T-Lit) (T-Add) (T-Sub) trait ArithExprSym { type Repr[T] def lit(n: Int): Repr[Int] def add(x: Repr[Int], y: Repr[Int]): Repr[Int] def sub(x: Repr[Int], y: Repr[Int]): Repr[Int] } This interface encodes both syntax and types in a single type-safe structure.

Slide 27

Slide 27 text

© 2024 Loglass Inc. 27 03 | The Tagless-Final Approach Writing User Programs Without Semantics def program(using s: ArithExprSym): s.Repr[Int] = { import s._ add(int(1), sub(lit(2), lit(3))) } 1 + (2 - 3) In Tagless-Final, we can write programs before defining their semantics. ● No semantics needed at this point ● Focus purely on the structure and rules of the language ● Enables early validation of business logic ● Decouples language usage from its interpretation ● Supports multiple interpretations later (e.g. evaluation, logging)

Slide 28

Slide 28 text

© 2024 Loglass Inc. 28 In Tagless-Final, you define what to say before defining what it means.

Slide 29

Slide 29 text

© 2024 Loglass Inc. 29 03 | The Tagless-Final Approach Implementing Semantics: Evaluator case class R[T](unR: T) object Eval extends ArithExprSym { type Repr[T] = R[T] def lit(n: Int): R[Int] = R(n) def add(x: R[Int], y: R[Int]): R[Int] = R(x.unR + y.unR) def sub(x: R[Int], y: R[Int]): R[Int] = R(x.unR - y.unR) } def run[T](r: R[T]): T = r.unR @main def main(): Unit = val p = program(using Eval) println(run(p)) // Output: 0 add(lit(1), sub(lit(2), lit(3)))

Slide 30

Slide 30 text

© 2024 Loglass Inc. 30 03 | The Tagless-Final Approach Why Tagless-Final Matters Tagless-Final gives you structured syntax, type-safe construction, and pluggable semantics. ● One syntax → many semantics ● Type-safe by construction (no ill-typed programs) ● No ASTs(Abstract Syntax Tree) ● Easy to extend with new interpreters ● Great for business rules, DSLs, and simulation Tagless-Final: build once, interpret many times—safely.

Slide 31

Slide 31 text

© 2024 Loglass Inc. 31 04 Applying the Language: Fiscal Periods

Slide 32

Slide 32 text

© 2024 Loglass Inc. 32 04 | Applying the Language: Fiscal Periods Real-World Complexity: Fiscal Periods ● Fiscal periods are everywhere — in financial reporting, budgeting, analytics. ● But they differ across companies: ○ Some start in April, others in October ○ Some report monthly, others quarterly ○ Some need temporary closures or partial locks Months Quarters Years a year Q1 Q2 Q3 Q4 M1 M2 M3 M4 M5 M6 M7 M8 M9 M10 M11 M12

Slide 33

Slide 33 text

© 2024 Loglass Inc. 33 04 | Applying the Language: Fiscal Periods Designing a Language for Fiscal Periods: Syntax where Constructors Operations

Slide 34

Slide 34 text

© 2024 Loglass Inc. 34 04 | Applying the Language: Fiscal Periods The Type System of FiscalPeriod DSL where Constructors Operations

Slide 35

Slide 35 text

© 2024 Loglass Inc. 35 04 | Applying the Language: Fiscal Periods Semantics: Giving Meaning to Fiscal Periods where Constructors Operations where

Slide 36

Slide 36 text

© 2024 Loglass Inc. 36 04 | Applying the Language: Fiscal Periods Defining the DSL Interface with Tagless-Final trait FiscalPeriodSym: type Repr[A] // expression whose meaning is undecided def fiscalYear(n: Int): Repr[Year] def quarter(n: Int, q: Int): Repr[Quarter] … def startDate[A](p: Repr[A])(using IsPeriod[A]): Repr[Date] def union[A, B](p1: Repr[A], p2: Repr[B]) (using IsPeriod[A], IsPeriod[B]): Repr[Period] // IsPeriod[A] = proof that A can be treated like a Period …

Slide 37

Slide 37 text

© 2024 Loglass Inc. 37 04 | Applying the Language: Fiscal Periods Defining the DSL Interface with Tagless-Final trait FiscalPeriodSym: type Repr[A] // expression whose meaning is undecided def fiscalYear(n: Int): Repr[Year] def quarter(n: Int, q: Int): Repr[Quarter] … def startDate[A](p: Repr[A])(using IsPeriod[A]): Repr[Date] def union[A, B](p1: Repr[A], p2: Repr[B]) (using IsPeriod[A], IsPeriod[B]): Repr[Period] // IsPeriod[A] = proof that A can be treated like a Period …

Slide 38

Slide 38 text

© 2024 Loglass Inc. 38 04 | Applying the Language: Fiscal Periods Writing a Program in the DSL def keyOperationalPeriod[F /: FiscalPeriodSym](using F: F): F.Repr[Period] = import F.* union(quarter(2025, 1), quarter(2025, 2)) This is a complete program — but it has no meaning until we plug in an interpreter.

Slide 39

Slide 39 text

© 2024 Loglass Inc. 39 04 | Applying the Language: Fiscal Periods Implementing the Semantics with Logging final case class EvalWithLog(startMonth: Int) extends FiscalPeriodSym: type Repr[A] = (List[String], A) … def quarter(n: Int, q: Int): Repr[Quarter] = val m = ((startMonth + 3 * (q - 1) - 1) % 12) + 1 val start = LocalDate.of(n, m, 1) val end = start.plusMonths(3).minusDays(1) (List(s"quarter($n, $q)"), Quarter(Period(start, end))) … def union[A, B](p1: Repr[A], p2: Repr[B])(using IsPeriod[A], IsPeriod[B]): Repr[Period] = val (l1, v1) = p1 val (l2, v2) = p2 val a = summon[IsPeriod[A]].toPeriod(v1) val b = summon[IsPeriod[B]].toPeriod(v2) val r = Period(a.start min b.start, a.end max b.end) (l1 /+ l2 :+ s"union(${a.start}–${a.end}, ${b.start}–${b.end}) = ${r.start}–${r.end}", r) … The user program stays clean — and yet we can log every step for debugging.

Slide 40

Slide 40 text

© 2024 Loglass Inc. 40 04 | Applying the Language: Fiscal Periods Evaluating the Program with Logs def keyOperationalPeriod[F /: FiscalPeriodSym](using F: F): F.Repr[Period] = import F.* union(quarter(2025, 1), quarter(2025, 2)) val eval = EvalWithLog(startMonth = 4) val result = run(keyOperationalPeriod(using eval)) Previously implemented interpreter with logging //= Evaluation Trace //= quarter(2025, 1) quarter(2025, 2) union(2025-04-01–2025-06-30, 2025-07-01–2024-09-30) = 2025-04-01–2024-09-30 ======================== result: Period(2025-04-01, 2025-09-30) The user program stays clean — and yet we can trace every step. The user program

Slide 41

Slide 41 text

© 2024 Loglass Inc. 41 05 Advanced Topics: Algebras and Complexity

Slide 42

Slide 42 text

© 2024 Loglass Inc. 42 Algebra

Slide 43

Slide 43 text

© 2024 Loglass Inc. 43 05 | Advanced Topics: Algebras and Complexity Why Algebra? – From Operations to Structure Algebra gives structure to operations. It helps us reason about what can be done and what must hold. ● A DSL defines operations, but not all operations are safe. ● Without laws, operations become meaningless or dangerous. ● Algebra provides a disciplined way to attach meaning to operations. Domain logic is not just about what you can do,  but also about how things are expected to behave.

Slide 44

Slide 44 text

© 2024 Loglass Inc. 44 05 | Advanced Topics: Algebras and Complexity What Is an Algebra? — Set, Operations, Laws Algebra = (A {operations}, {laws}) ● A: the carrier set – the domain of values the operations act on (e.g. Int, String, Period) ● Operations: functions defined over A (e.g. +, intersection) ● Laws: rules those operations must satisfy (e.g. associativity, identify) Example:

Slide 45

Slide 45 text

© 2024 Loglass Inc. 45 05 | Advanced Topics: Algebras and Complexity A DSL Is a Small Algebra Designing a DSL means designing: ● a carrier set ● operations on that set ● and the laws they must satisfy Example Law 1: Commutativity of Intersect Example Law 2: Rollup preserves containment intersect(a, b) == intersect(b, a) if contains(a, b) then contains(rollup(a), rollup(b))

Slide 46

Slide 46 text

© 2024 Loglass Inc. 46 Algebra = (Carrier Set, Operations, Laws) Tagless-Final maps each component into code: 05 | Advanced Topics: Algebras and Complexity Tagless-Final Realizes Algebra trait FiscalPeriodDSL[Repr[_]]: def periodDay(start: LocalDate, endExclusive: LocalDate): Repr[Period[Day]] def periodMonth(from: FiscalMonth, to: FiscalMonth): Repr[Period[Month]] def periodQuarter(from: FiscalQuarter, to: FiscalQuarter): Repr[Period[Quarter]] def periodYear(from: FiscalYear, to: FiscalYear): Repr[Period[Year]] def monthsIn(p: Repr[Period[Day]]): Repr[List[FiscalMonth]] def intersect[G /: Grain](a: Repr[Period[G]], b: Repr[Period[G]]): Repr[Option[Period[G]]] def contains[G /: Grain](outer: Repr[Period[G]], inner: Repr[Period[G]]): Repr[Boolean] def rollup[G1 /: Grain, G2 /: Grain](p: Repr[Period[G1]])(using Promote[G1, G2]): Repr[Period[G2]] Carrier set Operations Laws are not defined in the trait itself, but every interpreter of the DSL must respect them.

Slide 47

Slide 47 text

© 2024 Loglass Inc. 47 05 | Advanced Topics: Algebras and Complexity How to Guarantee Laws Laws express the expected behavior of operations. In Tagless-Final, they are guaranteed outside the trait. intersect(a, b) == intersect(b, a) Example Law: Commutativity of intersect

Slide 48

Slide 48 text

© 2024 Loglass Inc. 48 05 | Advanced Topics: Algebras and Complexity Property-Based Testing (PBT) property("intersect is commutative") = forAll { (a, b) /> eval.intersect(a, b) /= eval.intersect(b, a) } Instead of writing example-based tests, define laws as general properties and test them over many inputs. ● Write the law as a testable property ● Use randomized or exhaustive inputs ● Detect violations through counterexamples Best for: ● Commutativity, associativity, idempotence ● Properties that are easy to falsify, but hard to prove statically

Slide 49

Slide 49 text

© 2024 Loglass Inc. 49 05 | Advanced Topics: Algebras and Complexity Proof-Based Verification Use a proof assistant (like Lean or Coq) to establish that your laws always hold — mathematically. ● Encode your domain and operations as formal logic. ● Prove that each law holds for all possible inputs, not just examples. Best for: ● Laws that are too subtle to test reliably ● Foundational correctness of interpreters ● Domain invariants that must never break theorem intersect_comm (a b : Period) : intersect a b = intersect b a

Slide 50

Slide 50 text

© 2024 Loglass Inc. 50 Accidental Complexity

Slide 51

Slide 51 text

© 2024 Loglass Inc. 51 05 | Advanced Topics: Algebras and Complexity Controlling Accidental Complexity with DSLs When structure and semantics are separated, complexity can't leak in. ● Mixing logic with effects ● Scattering business intent across layers ● Semantics embedded in code ● Difficult to test or reason about ● Business logic is expressed as structure ● Types enforce valid composition ● Semantics are injected via interpreters ● Logic stays pure, semantics stay swappable Why accidental complexity happens How DSLs help contain it

Slide 52

Slide 52 text

© 2024 Loglass Inc. 52 06 Summary

Slide 53

Slide 53 text

© 2024 Loglass Inc. 53 06 | Summary ✔ Domain models tend to fall apart when treated as mere data holders. ✔ Thinking of a model as a language gives it structure and interpretability. ✔ Tagless-Final enables us to define such languages safely and modularly. ✔ By designing syntax, typing rules, and semantics explicitly, we regain control. ✔ Ultimately, domain models are algebras — this is not just theory, but a design principle. Summary

Slide 54

Slide 54 text

© 2024 Loglass Inc. 54 By treating the domain as a language, we can encode business intent precisely and safely. Tagless-Final offers a lightweight and modular way to do just that.

Slide 55

Slide 55 text

© 2024 Loglass Inc. 55 Appendix

Slide 56

Slide 56 text

© 2024 Loglass Inc. 56 Source Code https://github.com/knih/fiscal-period fiscal-period: A type-safe and extensible DSL for computing and working with fiscal periods, implemented in the tagless-final style.