Skip to content

Instantly share code, notes, and snippets.

@nrinaudo
Last active April 21, 2025 20:18
Show Gist options
  • Save nrinaudo/bff49d702cf42b9bc4742f0c445f3600 to your computer and use it in GitHub Desktop.
Save nrinaudo/bff49d702cf42b9bc4742f0c445f3600 to your computer and use it in GitHub Desktop.
Tagless Final as context functions
//> using scala 3
// This demonstrates a Scala 3 based Tagless Final encoding, where Tagless Final is taken in its original,
// Kiselyov meaning: an answer to the expression problem which relies on type classes.
//
// Each DSL is described by a "symantic" (sigh), a trait that describes both the syntax of the DSL (and
// instances of which describe the semantics, hence the dreadful mot valise).
//
// A value of our DSL is then of type [F[_]] => Symantic[F] ?=> F[A], which allows us to plug any output
// type we want.
//
// The following bit of code demonstrates what I actually mean by this.
//
// The only upsetting flaw is how you declare expressions in a DSL, which is rather syntax heavy. See
// the declaration of `test` for an example...
// There's already a SIP to tackle this: https://docs.scala-lang.org/sips/polymorphic-eta-expansion.html
// Which, of course, doesn't guarantee it will get done, but it does increase the odds.
// - Numbers DSL ---------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------
// We only support number literals and their equality.
trait NumSym[F[_]]:
def num(value: Int): F[Int]
extension (lhs: F[Int]) def =?=(rhs: F[Int]): F[Boolean]
def num[F[_]](i: Int)(using sym: NumSym[F]) = sym.num(i)
// Useful type alias because dear god.
type NumRule[A] = [F[_]] => NumSym[F] ?=> F[A]
// - Booleans DSL --------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------
// Essentially the same thing, but for booleans.
trait BoolSym[F[_]]:
def bool(value: Boolean): F[Boolean]
extension (lhs: F[Boolean]) def &&(rhs: F[Boolean]): F[Boolean]
def bool[F[_]](b: Boolean)(using sym: BoolSym[F]) = sym.bool(b)
type BoolRule[A] = [F[_]] => BoolSym[F] ?=> F[A]
// - Pretty printing -----------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------
// A symantic of `Pretty` is used to pretty print expressions in our DSLs.
// This is the "add new behaviours to the datatype" branch of the expression problem.
type Pretty[A] = String
// Pretty printer for numbers.
given NumSym[Pretty]:
def num(value: Int) = value.toString
extension (lhs: String) def =?=(rhs: String) = s"$lhs == $rhs"
val prettyNum = [F[_]] => (_: NumSym[F]) ?=> num(1) =?= num(2)
println(prettyNum[Pretty])
// Pretty printer for booleans.
given BoolSym[Pretty]:
def bool(value: Boolean) = value.toString
extension (lhs: String) def &&(rhs: String) = s"$lhs && $rhs"
val prettyBool = [F[_]] => (_: BoolSym[F]) ?=> bool(true) && bool(false)
println(prettyBool[Pretty])
// - Combined DSLs -------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------
// Rule combines both NumSym and BoolSym.
// This is the "add new cases to the datatype" branch of the expression problem.
type Rule[A] = [F[_]] => (NumSym[F], BoolSym[F]) ?=> F[A]
// Creates a "rule" that uses both `NumSym` and `BoolSym`.
// The syntax is dreadful, of course, but there's hope.
// If https://docs.scala-lang.org/sips/polymorphic-eta-expansion.html gets implemented,
// this will be rewritten as:
// val test: Rule[Boolean] = (num(1) =?= num(2)) && bool(true)
// Which I honestly think is quite nice.
val test: Rule[Boolean] = [F[_]] =>
(_: NumSym[F], _: BoolSym[F]) ?=> (num(1) =?= num(2)) && bool(true)
// prints `1 == 2 && true`
// Note how we did not have to write any new `Pretty` code. The implementations
// we wrote for `NumSym` and `BoolSym` are simply picked up and combined with no
// additional work.
println(test[Pretty])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment