Created
December 10, 2020 06:26
-
-
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
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
// 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] |
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
// 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) |
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
// 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)) |
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
// 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)) |
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
// 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