Skip to content

Instantly share code, notes, and snippets.

@kubukoz
Forked from arosien/finally-noel.scala
Last active March 29, 2025 13:19
Show Gist options
  • Save kubukoz/050d0bdc61195467ecce25ec6dd23800 to your computer and use it in GitHub Desktop.
Save kubukoz/050d0bdc61195467ecce25ec6dd23800 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
// 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(
alg: Alg
): alg.Ui[A]
// Define constructors returning programs
object Controls:
def text(prompt: String): Program[Controls, String] = _.text(prompt)
def choice(
prompt: String,
options: (String, Boolean)*
): Program[Controls, Boolean] = _.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)] = alg => alg.and(p(alg), second(alg))
// Reached our goal
val ui = Controls
.text("What is your name?")
.and(
Controls.choice(
"Are you enjoying Scalar?",
"Yes" -> true,
"Heck yes!" -> true,
)
)
//> 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(Tokenize).getConst.pretty)
/*
and
text: What is your name?
choice: Are you enjoying Scalar?, (Yes,true), (Heck yes!,true)
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment