Created
March 28, 2025 23:13
-
-
Save arosien/f5bfeb315e5447768bf75261aa79e531 to your computer and use it in GitHub Desktop.
tagless final implementation from noel welsh's talk "tagless final for humans" https://noelwelsh.com/talks/tagless-final-for-humans
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://noelwelsh.com/talks/tagless-final-for-humans | |
trait Algebra: | |
type Ui[_] | |
trait Controls extends Algebra: | |
def text(prompt: String): Ui[String] | |
def choice(prompt: String, options: (String, Boolean)*): Ui[Boolean] | |
// etc... | |
trait Layout extends Algebra: | |
def and[A, B](t: Ui[A], b: Ui[B]): Ui[(A, B)] | |
// Declare a type for programs | |
trait Program[-Alg <: Algebra, A]: | |
// NOTE: delays choice of algebra to after the program is defined | |
def apply(using alg: Alg): alg.Ui[A] | |
// Define constructors returning programs | |
object Controls: | |
def text(prompt: String): Program[Controls, String] = | |
new Program[Controls, String]: | |
def apply(using alg: Controls): alg.Ui[String] = | |
alg.text(prompt) | |
def choice( | |
prompt: String, | |
options: (String, Boolean)* | |
): Program[Controls, Boolean] = | |
new Program[Controls, Boolean]: | |
def apply(using alg: Controls): alg.Ui[Boolean] = | |
alg.choice(prompt, options*) | |
// 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)] = | |
new Program[Alg & Alg2 & Layout, (A, B)]: | |
def apply(using alg: Alg & Alg2 & Layout): alg.Ui[(A, B)] = | |
alg.and(p.apply, second.apply) | |
// Reached our goal | |
val ui = | |
Controls | |
.text("What is your name?") | |
.and( | |
Controls.choice( | |
"Are you enjoying Scalar?", | |
"Yes" -> true, | |
"Heck yes!" -> true | |
) | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//> using toolkit typelevel:0.1.29 | |
// example interpreter of the program | |
import cats.data.Const | |
import cats.syntax.all.* | |
// convert the UI to a sequence of tokens for pretty printing | |
object Tokenize extends Controls, Layout: | |
// pretend you're an A, but really you're an (accumulating) list of tokens | |
type Ui[A] = Const[List[Token], A] | |
enum Token: | |
case Indent | |
case Undent | |
case Pretty(value: String) | |
extension (tokens: List[Token]) | |
def pretty: String = | |
def go(tokens: List[Token], indent: Int): String = | |
tokens match | |
case Nil => "" | |
case Token.Indent :: next => go(next, indent + 1) | |
case Token.Undent :: next => go(next, indent - 1) | |
case Token.Pretty(value) :: next => (" " * indent) + value + "\n" + go(next, indent) | |
go(tokens, 0) | |
// conjunction is the semigroupal product | |
def and[A, B](a: Ui[A], b: Ui[B]): Ui[(A, B)] = | |
Const.of(List(Token.Pretty("and"), Token.Indent)) *> | |
(a, b).tupled <* | |
Const.of(List(Token.Undent)) | |
def choice(prompt: String, options: (String, Boolean)*): Ui[Boolean] = | |
Const.of(List(Token.Pretty(s"choice: $prompt, ${options.mkString(", ")}"))) | |
def text(prompt: String): Ui[String] = | |
Const.of(List(Token.Pretty(s"text: $prompt"))) | |
@main | |
def main = | |
println(ui(using Tokenize).getConst.pretty) | |
/* | |
and | |
text: What is your name? | |
choice: Are you enjoying Scalar?, (Yes,true), (Heck yes!,true) | |
*/ |
@noelwelsh ah nice. do you prefer the type parameter in the algebras like your snippet? or is it more of a user experience enhancement where you use a type-member instead, which you sort of suggested in your talk?
No, type members are better IMO. The code is just what I had at hand from the demo system I wrote for the talk, which uses the old tagless final (Ye Olde Tagless Final). Similar code in the more modern style can be found at https://github.com/noelwelsh/aui and https://github.com/creativescala/gooey/
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's another interpreter, which doesn't quite match the talk but shows output on the terminal.