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

From Domain Models to Domain Languages with Tag...

From Domain Models to Domain Languages with Tagless-Final

This slide deck, presented at GIDS 2025, explores how the Tagless-Final approach can be used to design robust, algebraic domain-specific languages (DSLs) that express domain logic in a composable and verifiable way. Using the example of a fiscal period DSL, we demonstrate how separating syntax, types, and semantics leads to maintainable, scalable domain models. Ideal for those interested in functional programming, DDD, and language-oriented design.

Avatar for Kenichi SUZUKI

Kenichi SUZUKI

April 24, 2025
Tweet

More Decks by Kenichi SUZUKI

Other Decks in Programming

Transcript

  1. © 2024 Loglass Inc. 1 © 2024 Loglass Inc. Designing

    for Contextual Integrity From Domain Models To Domain Languages with Tagless-Final Kenichi Suzuki Loglass Inc.
  2. © 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.
  3. © 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
  4. © 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
  5. © 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.
  6. © 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.
  7. © 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.
  8. © 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()
  9. © 2024 Loglass Inc. 11 02 Model as Language Preserving

    meaning through structural expression
  10. © 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
  11. © 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.
  12. © 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
  13. © 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
  14. © 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.
  15. © 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.
  16. © 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
  17. © 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.
  18. © 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.
  19. © 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.
  20. © 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.
  21. © 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.
  22. © 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.
  23. © 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.
  24. © 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)
  25. © 2024 Loglass Inc. 28 In Tagless-Final, you define what

    to say before defining what it means.
  26. © 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)))
  27. © 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.
  28. © 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
  29. © 2024 Loglass Inc. 33 04 | Applying the Language:

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

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

    Fiscal Periods Semantics: Giving Meaning to Fiscal Periods where Constructors Operations where
  32. © 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 …
  33. © 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 …
  34. © 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.
  35. © 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.
  36. © 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
  37. © 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.
  38. © 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:
  39. © 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))
  40. © 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.
  41. © 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
  42. © 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
  43. © 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
  44. © 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
  45. © 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
  46. © 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.
  47. © 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.