Skip to content

Instantly share code, notes, and snippets.

@blast-hardcheese
Created December 10, 2020 06:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blast-hardcheese/731ebb0f69dc18f864751e01e95e5db6 to your computer and use it in GitHub Desktop.
Save blast-hardcheese/731ebb0f69dc18f864751e01e95e5db6 to your computer and use it in GitHub Desktop.
Higher-order monadic syntax leveraging Scala's typesystem for consistency and correctness
// Need to be able to support different AST types
class LanguageAbstraction {
type Term
type Apply
}
// Equivalent to case class Foo[A](value: A), but with two additional properties:
// - L <: LanguageAbstraction, for letting us specify the language of what's inside
// - Z , for letting us describe the type of what's inside
case class Phantom[A, L <: LanguageAbstraction, Z](value: A)
// Define Monad with the addition of LanguageAbstraction so we can operate over Phantom[A, L, Z]
trait MonadF[L <: LanguageAbstraction, F[_]] {
def map[First <: L#Term, Func <: L#Term, From, To](f: Phantom[Func, L, From => To])(fa: Phantom[First, L, F[From]]): Phantom[L#Apply, L, F[To]]
def flatMap[First <: L#Term, Func <: L#Term, From, To](f: Phantom[Func, L, From => F[To]])(fa: Phantom[First, L, F[From]]): Phantom[L#Apply, L, F[To]]
}
// Syntax fo' convenience
implicit class PhantomSyntax[L <: LanguageAbstraction, First <: L#Term, F[_], From](fa: Phantom[First, L, F[From]])(implicit ev: MonadF[L, F]) {
def map[Func <: L#Term, To](f: Phantom[Func, L, From => To]): Phantom[L#Apply, L, F[To]] = ev.map(f)(fa)
def flatMap[Func <: L#Term, To](f: Phantom[Func, L, From => F[To]]): Phantom[L#Apply, L, F[To]] = ev.flatMap(f)(fa)
}
// Creating Phantom sucks, let's make it less sucky
class LiftHolder[L <: LanguageAbstraction, Z](value: String = null) {
def apply[A <: L#Term](fa: A) = Phantom[A, L, Z](fa)
}
def lift[L <: LanguageAbstraction, Z] = new LiftHolder[L, Z]
// Define Scala in terms of the scala.meta library terms
class ScalaLanguage extends LanguageAbstraction {
type Term = scala.meta.Term
type Apply = scala.meta.Term.Apply
}
implicit object scalaMetaOptional extends MonadF[ScalaLanguage, Option] {
import scala.meta._
def map[First <: Term, Func <: Term, From, To](f: Phantom[Func, ScalaLanguage, From => To])(fa: Phantom[First, ScalaLanguage, Option[From]]) =
Phantom[Term.Apply, ScalaLanguage, Option[To]](q"${fa.value}.map(${f.value})")
def flatMap[First <: Term, Func <: Term, From, To](f: Phantom[Func, ScalaLanguage, From => Option[To]])(fa: Phantom[First, ScalaLanguage, Option[From]]) =
Phantom[Term.Apply, ScalaLanguage, Option[To]](q"${fa.value}.flatMap(${f.value})")
}
// Convenience function for creating new things represented in our domain
def liftScala[Z] = lift[ScalaLanguage, Z]
// Define a simple sequence of operations
val fa = liftScala[Option[Int]](q"Option(5)")
val f = liftScala[Int => Option[Int]](q"a => if(a >= 0) Some(a) else None")
val f2 = liftScala[Int => Int](q"a => a + 1")
println(fa.flatMap(f).map(f2).value.syntax)
// Prints Option(5).flatMap(a => if (a >= 0) Some(a) else None).map(a => a + 1)
// No AST for Javascript easily available, just use String and hope for the best
class JavascriptLanguage extends LanguageAbstraction {
type Term = String
type Apply = String
}
// Everything is defined in terms of String, verbosely for no external deps. This could easily be swapped out for lodash or something.
implicit object jsOptional extends MonadF[JavascriptLanguage, Option] {
def map[First <: String, Func <: String, From, To](f: Phantom[Func, JavascriptLanguage, From => To])(fa: Phantom[First, JavascriptLanguage, Option[From]]) =
Phantom[String, JavascriptLanguage, Option[To]](s"(x => { if (typeof x !== undefined && x !== null) { return (${f.value})(x); } else { return x; } })(${fa.value})")
def flatMap[First <: String, Func <: String, From, To](f: Phantom[Func, JavascriptLanguage, From => Option[To]])(fa: Phantom[First, JavascriptLanguage, Option[From]]) =
Phantom[String, JavascriptLanguage, Option[To]](s"(x => { if (typeof x !== undefined && x !== null) { return (${f.value})(x); } else { return x; } })(${fa.value})")
}
def liftJS[Z] = lift[JavascriptLanguage, Z]
// Define our dataflow in JS syntax
val fa = liftJS[Option[Int]]("5")
val f = liftJS[Int => Option[Int]]("a => (a >= 0) ? a : null")
val f2 = liftJS[Int => Int]("a => a + 1")
println(fa.flatMap(f).map(f2).value)
// Prints (x => { if (typeof x !== undefined && x !== null) { return (a => a + 1)(x); } else { return x; } })((x => { if (typeof x !== undefined && x !== null) { return (a => (a >= 0) ? a : null)(x); } else { return x; } })(5))
// Again, no AST for python easily available
class PythonLanguage extends LanguageAbstraction {
type Term = String
type Apply = String
}
// Still stringy, :fingerscrossed:
implicit object pythonOptional extends MonadF[PythonLanguage, Option] {
def map[First <: String, Func <: String, From, To](f: Phantom[Func, PythonLanguage, From => To])(fa: Phantom[First, PythonLanguage, Option[From]]) =
Phantom[String, PythonLanguage, Option[To]](s"(lambda x: (${f.value})(x) if x is not None else x)(${fa.value})")
def flatMap[First <: String, Func <: String, From, To](f: Phantom[Func, PythonLanguage, From => Option[To]])(fa: Phantom[First, PythonLanguage, Option[From]]) =
Phantom[String, PythonLanguage, Option[To]](s"(lambda x: (${f.value})(x) if x is not None else x)(${fa.value})")
}
// Convenience method
def liftPy[Z] = lift[PythonLanguage, Z]
// Dataflow
val fa = liftPy[Option[Int]]("5")
val f = liftPy[Int => Option[Int]]("lambda a: a if a >= 0 else None")
val f2 = liftPy[Int => Int]("lambda a: a + 1")
println(fa.flatMap(f).map(f2).value)
// Prints (lambda x: (lambda a: a + 1)(x) if x is not None else x)((lambda x: (lambda a: a if a >= 0 else None)(x) if x is not None else x)(5))
// No AST for haskell easily available
class HaskellLanguage extends LanguageAbstraction {
type Term = String
type Apply = String
}
// Instances for Haskell
implicit object haskellOptional extends MonadF[HaskellLanguage, Option] {
def map[First <: String, Func <: String, From, To](f: Phantom[Func, HaskellLanguage, From => To])(fa: Phantom[First, HaskellLanguage, Option[From]]) =
Phantom[String, HaskellLanguage, Option[To]](s"fmap (${f.value}) (${fa.value})")
def flatMap[First <: String, Func <: String, From, To](f: Phantom[Func, HaskellLanguage, From => Option[To]])(fa: Phantom[First, HaskellLanguage, Option[From]]) =
Phantom[String, HaskellLanguage, Option[To]](s"(${fa.value}) >>= (${f.value})")
}
// Convenience
def liftHs[Z] = lift[HaskellLanguage, Z]
// Program
val fa = liftHs[Option[Int]]("Just 5")
val f = liftHs[Int => Option[Int]]("(mfilter (>= 0)) . (pure :: a -> Maybe a)")
val f2 = liftHs[Int => Int]("+ 1")
println(fa.flatMap(f).map(f2).value)
// Prints fmap (+ 1) ((Just 5) >>= ((mfilter (>= 0)) . (pure :: a -> Maybe a)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment