Last active
April 21, 2025 20:18
-
-
Save nrinaudo/bff49d702cf42b9bc4742f0c445f3600 to your computer and use it in GitHub Desktop.
Tagless Final as context functions
This file contains hidden or 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 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