Cast thine eyes upon this example
| def scalar[Ui[_]: Controls: Layout](): Ui[(String, Boolean)] = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
| |
| val consoleUi = scalar[Console]() |
| val (name, enjoyment) = consoleUi() |
This doeth be a program
| def scalar[Ui[_]: Controls: Layout](): Ui[(String, Boolean)] = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
| |
| val consoleUi = scalar[Console]() |
| val (name, enjoyment) = consoleUi() |
Verily, this be an interpreter
| def scalar[Ui[_]: Controls: Layout](): Ui[(String, Boolean)] = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
| |
| val consoleUi = scalar[Console]() |
| val (name, enjoyment) = consoleUi() |
Thy program calls methods on thine context parameters
| def scalar[Ui[_]: Controls: Layout](): Ui[(String, Boolean)] = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
| |
| val consoleUi = scalar[Console]() |
| val (name, enjoyment) = consoleUi() |
We shall know these context parameters as program algebras
Program algebras shall consist of constructors…
| |
| Controls[Ui].text("What is your name?"), |
and combinators
| |
| Layout[Ui].and(firstUi, secondUi) |
With more program algebras thine program shall be extended
| def scalar[Ui[_]: Controls: Layout]() = ... |
| def scalar[Ui[_]: Controls: Layout: Events]() = ... |
A new implementation of thy program algebras shall bring a new interpretation
| val webUi = scalar[Dom]() |
| webUi.mountAtId("mount-point") |
With tagless final, thou canst extend thine program and extend thine interpretation
Ye Olde Tagless Final is tedious to write
| def scalar[Ui[_]: Controls: Layout]() = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
Ye Olde Tagless Final raises barriers to entry
Higher-kinded types, given values, using clauses, context bounds
Ye Olde Tagless Final puts the burden on the wrong person
A little more work by the library author
| val ui = |
| Controls |
| .text("What is your name?") |
| .and( |
| Controls.choice( |
| "Are you enjoying Scalar?", |
| Seq("Yes" -> true, "Heck yes!" -> true) |
| ) |
| ) |
The five-step plan
- Subtyping for program algebras
- Type member for result type
- A program type
- Constructors
- Extension methods for combinators
Declare a base type for all program algebras
| trait Algebra[Ui[_]] |
| |
| trait Controls[Ui[_]] extends Algebra[Ui] { |
| def text(prompt: String): Ui[String] |
| |
| } |
| trait Layout[Ui[_]] extends Algebra[Ui] { |
| def and[A, B](t: Ui[A], b: Ui[B]): Ui[(A, B)] |
| } |
| |
Make the result type a type member
| trait Algebra { |
| type Ui[_] |
| } |
| |
| trait Controls extends Algebra { |
| def text(prompt: String): Ui[String] |
| |
| } |
| trait Layout extends Algebra { |
| def and[A, B](t: Ui[A], b: Ui[B]): Ui[(A, B)] |
| } |
| |
Let's see how this changes our example
| def scalar[Ui[_]: Controls: Layout](): Ui[(String, Boolean)] = |
| Layout[Ui].and( |
| Controls[Ui].text("What is your name?"), |
| Controls[Ui].choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
Context bounds becomes single value
| def scalar()( |
| using alg: Controls & Layout |
| ): alg.Ui[(String, Boolean)] = |
| alg.and( |
| alg.text("What is your name?"), |
| alg.choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
Use method dependent type
| def scalar()( |
| using alg: Controls & Layout |
| ): alg.Ui[(String, Boolean)] = |
| alg.and( |
| alg.text("What is your name?"), |
| alg.choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
Declare a type for programs
| trait Program[-Alg <: Algebra, A] { |
| def apply(using alg: Alg): alg.Ui[A] |
| } |
Programs are now values
| val scalar = |
| Program[Controls & Layout, (String, Boolean)] { |
| def apply(using alg: Controls & Layout) = |
| alg.and( |
| alg.text("What is your name?"), |
| alg.choice( |
| "Are you enjoying Scalar?", |
| "Yes" -> true, |
| "Heck yes!" -> true |
| ) |
| ) |
| } |
Define constructors returning programs
| object Controls { |
| def text(prompt: String): Program[Controls, String] = |
| Program[Controls, String] { |
| def apply(using alg: Controls): alg.Ui[String] = |
| alg.text(prompt) |
| } |
| } |
Define combinators using extension methods
| extension [Alg <: Algebra, A](p: Program[Alg, A]) { |
| def and[Alg2 <: Algebra, B]( |
| second: Program[Alg2, B] |
| ): Program[Alg & Alg2 & Layout, (A, B)] = |
| Program[Alg & Alg2 & Layout, (A, B)] { |
| def apply(using alg: Alg & Alg2 & Layout): alg.Ui[(A, B)] = |
| alg.and(p, second) |
| } |
Automatically accumulates required algebras
| extension [Alg <: Algebra, A](p: Program[Alg, A]) { |
| def and[Alg2 <: Algebra, B]( |
| second: Program[Alg2, B] |
| ): Program[Alg & Alg2 & Layout, (A, B)] = |
| Program[Alg & Alg2 & Layout, (A, B)] { |
| def apply(using alg: Alg & Alg2 & Layout): alg.Ui[(A, B)] = |
| alg.and(p, second) |
| } |
Example
| val c1: Program[Controls, String] = Controls.text(...) |
| val c2: Program[Controls, Boolean] = Controls.choice(...) |
| |
| |
| |
| c1.and(c2) |
Reached our goal
| val ui = |
| Controls |
| .text("What is your name?") |
| .and( |
| Controls.choice( |
| "Are you enjoying Scalar?", |
| Seq("Yes" -> true, "Heck yes!" -> true) |
| ) |
| ) |
Recap
User code reads like normal code
Extend functionality for platform specific actions (e.g. mobile functionality like location)
Extend interpretations for different platforms (e.g. new UI toolkits)