├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── core └── src │ ├── main │ └── scala │ │ └── tfm │ │ └── fin.scala │ └── test │ └── scala │ └── tfm │ └── finTests.scala ├── examples └── src │ └── main │ └── scala │ └── tfm │ └── examples │ ├── Example.scala │ ├── Free.scala │ ├── List.scala │ ├── Product.scala │ ├── Sum.scala │ └── Terminal.scala └── project └── build.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | .cache 15 | .classpath 16 | .project 17 | .worksheet/ 18 | bin/ 19 | .settings/ 20 | 21 | # OS X 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Adelbert Chang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfm 2 | tfm is "tagless final macro" - the project is intended to eliminate the boilerplate 3 | associated with setting up an EDSL encoded in the finally tagless approach, specifically 4 | the approach taken in 5 | [this article](https://pchiusano.github.io/2014-05-20/scala-gadts.html). 6 | 7 | ### Documentation 8 | Currently the documentation is all in the [Scaladoc](core/src/main/scala/tfm/fin.scala). 9 | Examples can be found in the [examples](examples/src/main/scala/tfm/examples) sub-project. 10 | 11 | ### Limitations 12 | * For algebras with effectful parameters (e.g. have shape `F[_]`) the macro will replace 13 | each occurence of `F` with the name of the algebra. However, if the `F[_]` appears as 14 | part of a more complex type (e.g. `A => F[B]`), the macro cannot figure out how to make 15 | the appropriate interpreter call and will fail. 16 | 17 | ### Reading 18 | * [Alternatives to GADTs in Scala](https://pchiusano.github.io/2014-05-20/scala-gadts.html) 19 | * The generated code is taken from this post 20 | * [Typed Tagless Interpretations](http://okmij.org/ftp/tagless-final/index.html) 21 | * [Folding Domain-Specific Languages: Deep and Shallow Embeddings](https://www.cs.ox.ac.uk/publications/publication7584-abstract.html) 22 | * [Combining Deep and Shallow Embedding for EDSL](http://www.cse.chalmers.se/~emax/documents/svenningsson2013combining.pdf) 23 | 24 | ### License 25 | Code is provided under the MIT license available at http://opensource.org/licenses/MIT, as well as the 26 | LICENSE file. 27 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val buildSettings = List( 2 | organization := "com.adelbertc", 3 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")), 4 | scalaVersion := "2.11.7", 5 | crossScalaVersions := List("2.10.5", scalaVersion.value), 6 | version := "0.1.0-SNAPSHOT" 7 | ) 8 | 9 | lazy val commonSettings = List( 10 | scalacOptions ++= List( 11 | "-deprecation", 12 | "-encoding", "UTF-8", 13 | "-feature", 14 | "-language:existentials", 15 | "-language:experimental.macros", 16 | "-language:higherKinds", 17 | "-language:implicitConversions", 18 | "-unchecked", 19 | "-Xfatal-warnings", 20 | "-Xlint", 21 | "-Yno-adapted-args", 22 | "-Yrangepos", 23 | "-Ywarn-dead-code", 24 | "-Ywarn-numeric-widen", 25 | "-Ywarn-value-discard" 26 | ), 27 | resolvers += Resolver.sonatypeRepo("snapshots"), 28 | libraryDependencies += 29 | compilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full), 30 | initialize := { System.setProperty("tfm.verbose", "") } 31 | ) 32 | 33 | lazy val tfmSettings = buildSettings ++ commonSettings 34 | 35 | lazy val tfm = 36 | project.in(file(".")). 37 | settings(tfmSettings). 38 | aggregate(core, examples) 39 | 40 | lazy val core = 41 | project.in(file("core")). 42 | settings(name := "tfm"). 43 | settings(description := "Annotation macro to generate EDSL code in the tagless final style"). 44 | settings(tfmSettings). 45 | settings( 46 | libraryDependencies ++= List( 47 | "org.typelevel" %% "macro-compat" % "1.0.1", 48 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 49 | "org.specs2" %% "specs2-core" % "3.6.4" % "test" 50 | ) 51 | ) 52 | 53 | lazy val examples = 54 | project.in(file("examples")). 55 | settings(tfmSettings). 56 | settings(libraryDependencies += "org.spire-math" %% "cats" % "0.2.0"). 57 | dependsOn(core) 58 | -------------------------------------------------------------------------------- /core/src/main/scala/tfm/fin.scala: -------------------------------------------------------------------------------- 1 | package tfm 2 | 3 | import macrocompat.bundle 4 | 5 | import scala.annotation.StaticAnnotation 6 | import scala.reflect.macros.whitebox.Context 7 | 8 | /** Annotation used to mark a field that should not be treated as part of the algebra. 9 | * 10 | * This becomes useful when you have interpreter-specific methods. 11 | */ 12 | class local extends StaticAnnotation 13 | 14 | /** Annotation used to mark a trait or class containing the algebra. Annotation must be 15 | * given a name to use as the name of the generated algebra. The annottee should be the 16 | * interpreter for the algebra. 17 | * 18 | * The interpreter must be parameterized by a type constructor `F[_]` - this type 19 | * constructor represents the effect the interpreter needs during interpretation. 20 | * The type constructor may have more than one type parameter. 21 | * 22 | * The macro will inspect and filter the public fields of the interpreter and generate 23 | * the appropriate algebra types and smart constructors accordingly. When the macro 24 | * is invoked, the following is generated and placed in the companion object of 25 | * the annottee: 26 | * 27 | * - A trait with the `algebraName`/first name provided to the annotation. This trait 28 | * contains the effectful algebra that the interpreter will eventually interpret. The trait 29 | * is parameterized by a (proper) type `A` (or more in the case of higher-arity effects) 30 | * which denotes the type of the value the underlying expression interprets to. The 31 | * trait contains a single method `run` on it parameterized by a type constructor `G` 32 | * with the same shape as the effect, and takes as input an instance of the interpreter 33 | * (the annottee) with type `[G]`. The output of `run` is `G` with 34 | * applied to the type parameters of the trait. This trait is used for fields that have 35 | * a (return) type where the outermost type constructor is the effect. 36 | * - A trait with the `auxAlgebraName`/second name provided to the annotation. This trait 37 | * is very similar to the algebra trait, but instead of `run` returning `G[A]`, it returns 38 | * just `A`. This trait is used for fields that do not have a (return) type where the 39 | * outermost type constructor is the effect. 40 | * - A trait named `Language` which contains the smart constructors for the algebra trait. 41 | * The smart constructors share the exact same names of the members of the algebra. The 42 | * smart constructors live in a trait instead of an object to allow potential library 43 | * authors to create an object that mixes in the smart constructors, along with any other 44 | * thing they may want. 45 | * - An object called `language` which extends the generated `Language` trait. This makes 46 | * the smart constructors easily available via importing `language._`. 47 | * 48 | * The algebra itself has the following restrictions: 49 | * - Each input parameter must either be of type with shape `F[..A]` or be of type that does 50 | * not contain `F[..A]`. For instance, `Int` and `[A]F[A]` are OK, but `A => F[B]` is not. 51 | */ 52 | class fin(algebraName: String, auxAlgebraName: String = "") extends StaticAnnotation { 53 | def macroTransform(annottees: Any*): Any = macro TfmMacro.generateAlgebra 54 | } 55 | 56 | @bundle 57 | class TfmMacro(val c: Context) { 58 | import c.universe._ 59 | 60 | private val algebraName = "algebraName" 61 | private val auxAlgebraName = "auxAlgebraName" 62 | 63 | def generateAlgebra(annottees: c.Expr[Any]*): c.Expr[Any] = { 64 | // A predicate used to test if a field could be part of the algebra 65 | def filterMods(mods: Modifiers): Boolean = 66 | isPublic(mods) && notOmitted(mods) 67 | 68 | def isPublic(mods: Modifiers): Boolean = { 69 | val blacklist = List(Flag.PRIVATE, Flag.PROTECTED) 70 | blacklist.forall(flag => !mods.hasFlag(flag)) 71 | } 72 | 73 | def notInit(name: TermName): Boolean = { 74 | val decoded = name.decodedName.toString 75 | (decoded != "$init$") && (decoded != "") 76 | } 77 | 78 | // Field does not contain the `local` annotation 79 | def notOmitted(mods: Modifiers): Boolean = 80 | mods.annotations.forall { 81 | case q"new local()" => false 82 | case q"new tfm.local()" => false 83 | case _ => true 84 | } 85 | 86 | // Verbose output if tfm.verbose system property is set 87 | def verbose(s: => String): Unit = 88 | if (sys.props.get("tfm.verbose").isDefined) c.info(c.enclosingPosition, s, false) 89 | 90 | // Check that `clazz` is paramterized by a type constructor 91 | def wellFormed(clazz: ClassDef): Boolean = 92 | clazz.tparams.map(_.tparams).headOption.toList.flatten.size > 0 93 | 94 | def generate(interpreter: ClassDef, interpreterModule: Option[ModuleDef]): c.Expr[Any] = { 95 | // Type parameter of annottee 96 | val _effect@q"${_} type ${effectName}[..${placeholders}] = ${_}" = interpreter.tparams.head 97 | val outer = _effect.duplicate 98 | 99 | // Type constructor is the same as the interpreter effect 100 | def isInterpreterEffect(tree: Tree): Boolean = { 101 | tree match { 102 | case tq"${outer}[..${inner}]" => 103 | outer match { 104 | case i: Ident if i.name.decodedName.toString == effectName.decodedName.toString => true 105 | case s: Select if s.name.decodedName.toString == effectName.decodedName.toString => true 106 | case _ => inner.exists(isInterpreterEffect) 107 | } 108 | case _ => false 109 | } 110 | } 111 | 112 | val interpreterName = interpreter.name 113 | val decodedInterpreterName = interpreterName.decodedName.toString 114 | 115 | // Get first argument of annotation to use as name of algebra 116 | val (algebraType, algebraTerm, interpreterReaderType) = 117 | c.prefix.tree match { 118 | case Apply(_, args) => 119 | val algebraString = 120 | args. 121 | collectFirst { case q"${name} = ${an}" if name.asInstanceOf[Ident].name.decodedName.toString == algebraName => an }. 122 | orElse(args.headOption). 123 | map(an => c.eval(c.Expr[String](an))). 124 | getOrElse(c.abort(c.enclosingPosition, s"Unspecified `${algebraName}` name to annotation (first parameter)")) 125 | 126 | val readerType = 127 | args. 128 | collectFirst { case q"${name} = ${an}" if name.asInstanceOf[Ident].name.decodedName.toString == auxAlgebraName => an }. 129 | orElse(scala.util.Try(args(1)).toOption). 130 | map(an => c.eval(c.Expr[String](an))). 131 | map { an => 132 | if (an == decodedInterpreterName) 133 | c.abort(c.enclosingPosition, s"Alternative algebra name cannot be same as that of the interpreter: ${decodedInterpreterName}") 134 | else if (an.isEmpty) 135 | c.abort(c.enclosingPosition, "Alternative algebra name cannot be empty string") 136 | else if (an == algebraString) 137 | c.abort(c.enclosingPosition, "Algebras cannot share names") 138 | else an 139 | }. 140 | map(rn => TypeName(rn)) 141 | 142 | if (algebraString == decodedInterpreterName) 143 | c.abort(c.enclosingPosition, s"Algebra name cannot be same as that of the interpreter: ${decodedInterpreterName}") 144 | else if (algebraString.isEmpty) 145 | c.abort(c.enclosingPosition, "Algebra name cannot be empty string") 146 | else (TypeName(algebraString), TermName(algebraString), readerType) 147 | } 148 | 149 | val algebrass = 150 | interpreter.impl.body.collect { 151 | // Method 152 | case q"${mods} def ${tname}[..${tparams}](...${paramss}): ${_outer}[..${inner}] = ${_}" if filterMods(mods) && notInit(tname) => 153 | // Process params - effectful params are processed differently from pure ones 154 | val newParamss = 155 | paramss.map(_.map { 156 | case q"${mods} val ${uname}: ${outer}[..${inner}] = ${expr}" => 157 | val innerDuplicate = inner.map(_.duplicate) 158 | val exprDuplicate = expr.duplicate 159 | // Parameter has shape F[A], must interpret parameter before making appropriate interpreter call 160 | if (isInterpreterEffect(outer)) 161 | (q"${mods} val ${uname}: ${algebraType}[..${innerDuplicate}] = ${exprDuplicate}", q"${uname}.run(interpreter)") 162 | // Parameter does not contain F[_], e.g. is A, List[String], Double => List[Char] 163 | else if (!inner.exists(isInterpreterEffect)) { 164 | val v = q"${mods} val ${uname}: ${outer.duplicate}[..${innerDuplicate}] = ${exprDuplicate}" 165 | (v, q"${uname}") 166 | } 167 | // F[_] appears in type of parameter, e.g. A => F[B] 168 | else 169 | c.abort(c.enclosingPosition, s"Parameter `${tname}: ${outer}[.. ${inner}]` has type containing effect '${effectName.decodedName.toString}'") 170 | }) 171 | 172 | val (args, valNames) = (newParamss.map(_.map(_._1)), newParamss.map(_.map(_._2))) 173 | val innerDuplicate = inner.map(_.duplicate) 174 | 175 | val tparamsDup = tparams.map(_.duplicate) 176 | if (isInterpreterEffect(_outer)) { 177 | Left(q""" 178 | def ${tname}[..${tparamsDup}](...${args}): ${algebraType}[..${innerDuplicate}] = 179 | new ${algebraType}[..${innerDuplicate}] { 180 | final def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): ${outer.name}[..${innerDuplicate}] = 181 | interpreter.${tname}(...${valNames}) 182 | } 183 | """) 184 | } else { 185 | Right((interpreterReaderType: TypeName) => { 186 | val r = q"${_outer.duplicate}[..${innerDuplicate}]" 187 | q""" 188 | def ${tname}[..${tparamsDup}](...${args}): ${interpreterReaderType}[${r}] = 189 | new ${interpreterReaderType}[${r}] { 190 | final def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): ${r} = 191 | interpreter.${tname}(...${valNames}) 192 | } 193 | """ 194 | }) 195 | } 196 | 197 | // Val 198 | case q"${mods} val ${tname}: ${_outer}[..${inner}] = ${_}" if filterMods(mods) => 199 | val innerDuplicate = inner.map(_.duplicate) 200 | 201 | if (isInterpreterEffect(_outer)) { 202 | Left(q""" 203 | val ${tname}: ${algebraType}[..${innerDuplicate}] = 204 | new ${algebraType}[..${innerDuplicate}] { 205 | final def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): ${outer.name}[..${innerDuplicate}] = 206 | interpreter.${tname} 207 | } 208 | """) 209 | } else { 210 | val t = q"${tname}: ${_outer}[..${inner}]" 211 | Right((interpreterReaderType: TypeName) => { 212 | val r = q"${_outer.duplicate}[..${innerDuplicate}]" 213 | q""" 214 | val ${tname}: ${interpreterReaderType}[${r}] = 215 | new ${interpreterReaderType}[${r}] { 216 | final def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): ${r} = 217 | interpreter.${tname} 218 | } 219 | """ 220 | }) 221 | } 222 | } 223 | 224 | val inner = placeholders.map(_ => q"type ${TypeName(c.freshName)}") 225 | val innerIdent = inner.map(n => Ident(n.name)) 226 | 227 | val (effectAlgebras, otherAlgebras) = 228 | algebrass.foldLeft((List.empty[ValOrDefDef], List.empty[TypeName => ValOrDefDef])) { 229 | case ((ds, vs), e) => e.fold(d => (d :: ds, vs), v => (ds, v :: vs)) 230 | } 231 | 232 | val (algebras, readerTrait) = 233 | interpreterReaderType match { 234 | case Some(irt) => 235 | val oas = otherAlgebras.map(_(irt)) 236 | val t = 237 | List(q""" 238 | trait ${irt}[A] { 239 | def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): A 240 | } 241 | """) 242 | (effectAlgebras ++ oas, t) 243 | case None => 244 | if (otherAlgebras.isEmpty) { 245 | (effectAlgebras, List()) 246 | } else { 247 | val names = otherAlgebras.map(_(TypeName("dummy"))).map(n => s"`${n.name.toString}`") 248 | val errMsg = s"Found parameters ${names.mkString(",")} with type different than the interpreter effect, but no interpreter reader name given - please provide as parameter `${auxAlgebraName}` (second parameter)" 249 | c.abort(c.enclosingPosition, errMsg) 250 | } 251 | } 252 | 253 | val generatedAlgebra = 254 | q""" 255 | trait ${algebraType}[..${inner}] { 256 | def run[${outer}](interpreter: ${interpreterName}[${outer.name}]): ${outer.name}[..${innerIdent}] 257 | } 258 | 259 | ..${readerTrait} 260 | 261 | trait Language { 262 | ..${algebras} 263 | } 264 | 265 | object language extends Language 266 | """ 267 | 268 | val interpreterCompanion = 269 | interpreterModule match { 270 | // Existing companion object, extended with the generated algebra 271 | case Some(q"${mods} object ${tname} extends { ..${earlydefns} } with ..${parents} { ${self} => ..${body} }") => 272 | val bodyDuplicate = body.map(_.duplicate) 273 | q"""${mods} object ${tname} extends { ..${earlydefns} } with ..${parents} { ${self} => 274 | ..${generatedAlgebra} 275 | ..${bodyDuplicate} 276 | } 277 | """ 278 | 279 | // No companion object, so generate one 280 | case None => 281 | q""" 282 | object ${interpreterName.toTermName} { 283 | ..${generatedAlgebra} 284 | } 285 | """ 286 | } 287 | 288 | verbose { 289 | s""" 290 | Algebras: 291 | --------- 292 | ${algebras.mkString("\n\n")} 293 | 294 | All: 295 | ---- 296 | ${interpreter.impl.body.mkString("\n\n")} 297 | """ 298 | } 299 | 300 | c.Expr(q""" 301 | ${interpreter.duplicate} 302 | 303 | ${interpreterCompanion} 304 | """) 305 | } 306 | 307 | annottees.map(_.tree) match { 308 | case (interpreter: ClassDef) :: Nil if wellFormed(interpreter) => 309 | generate(interpreter, None) 310 | case (interpreter: ClassDef) :: (interpreterModule: ModuleDef) :: Nil if wellFormed(interpreter) => 311 | generate(interpreter, Some(interpreterModule)) 312 | case _ => 313 | c.abort(c.enclosingPosition, "Annotation can only be applied to types parameterized with a type constructor") 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /core/src/test/scala/tfm/finTests.scala: -------------------------------------------------------------------------------- 1 | package tfm 2 | 3 | import org.specs2.Specification 4 | 5 | class finTests extends Specification { 6 | import Algebras._ 7 | 8 | def is = 9 | s2""" 10 | basic usage ${basicUsage} 11 | augments existing companion ${companion} 12 | allows abstract class interpreter ${abstractClass} 13 | supports implementation ${implementation} 14 | supports parametric methods ${parametric} 15 | supports higher-arity effects ${higherArity} 16 | supports effectful parameters ${effectful} 17 | supports non-effectful return type ${nonEffectful} 18 | supports named algebra ${namedAlgebra} 19 | supports named aux algebra ${namedAuxAlgebra} 20 | """ 21 | 22 | def basicUsage = { 23 | import BasicInterp._ 24 | import BasicInterp.language._ 25 | 26 | val x = 1 27 | val program1: BasicOp[Int] = lit(x) 28 | val program2: BasicOp[String] = name 29 | (program1.run(id) mustEqual x) && 30 | (program2.run(id) mustEqual s) 31 | } 32 | 33 | def companion = BasicInterp.id mustEqual BasicInterp.id 34 | 35 | def abstractClass = { 36 | import AbstractInterp._ 37 | import AbstractInterp.language._ 38 | 39 | val x = 5 40 | val program: AbstractOp[Int] = lit(x) 41 | program.run(id) mustEqual x 42 | } 43 | 44 | def implementation = { 45 | import ImplInterp._ 46 | import ImplInterp.language._ 47 | 48 | val x: ImplOp[Int] = foo(1, 2) 49 | val y: ImplOp[Int] = fooSwapped(1, 2) 50 | 51 | ok 52 | } 53 | 54 | def parametric = { 55 | import ParametricInterp._ 56 | import ParametricInterp.language._ 57 | 58 | val char: ParametricOp[Char] = lift('a') 59 | val int: ParametricOp[Int] = lift(1) 60 | val string: ParametricOp[String] = lift("hello") 61 | val vector: ParametricOp[Vector[Byte]] = lift(Vector.empty[Byte]) 62 | 63 | ok 64 | } 65 | 66 | def higherArity = { 67 | import ArityInterp._ 68 | val p: ArityOp[Int, String] = language.pair(1, "hello") 69 | ok 70 | } 71 | 72 | def effectful = { 73 | import EffectInterp._ 74 | import EffectInterp.language._ 75 | val program: EffectOp[Int] = add(lit(0), lit(1)) 76 | ok 77 | } 78 | 79 | def nonEffectful = { 80 | import NonEffectInterp._ 81 | import NonEffectInterp.language._ 82 | val i = 5 83 | val program1: NonEffectReader[Int] = identity(i) 84 | val program2: NonEffectReader[String] = aString 85 | (program1.run(id) mustEqual i) && 86 | (program2.run(id) mustEqual s) 87 | } 88 | 89 | def namedAlgebra = { 90 | import NamedAlgebraInterp._ 91 | import NamedAlgebraInterp.language._ 92 | val n = "named" 93 | val program1: NamedAlgebraOp[Int] = named(n) 94 | program1.run(id) mustEqual n.size 95 | } 96 | 97 | def namedAuxAlgebra = { 98 | import NamedAuxAlgebraInterp._ 99 | import NamedAuxAlgebraInterp.language._ 100 | val x = 5 101 | val program1: NamedAuxAlgebraOp2[Int] = noop(x) 102 | program1.run(id) mustEqual x 103 | } 104 | } 105 | 106 | object Algebras { 107 | type Id[A] = A 108 | 109 | @fin("BasicOp") 110 | trait BasicInterp[F[_]] { 111 | def lit(i: Int): F[Int] 112 | val name: F[String] 113 | } 114 | 115 | object BasicInterp { 116 | val s: String = "name" 117 | val id: BasicInterp[Id] = 118 | new BasicInterp[Id] { 119 | def lit(i: Int): Int = i 120 | val name: String = s 121 | } 122 | } 123 | 124 | @fin("AbstractOp") 125 | abstract class AbstractInterp[F[_]] { 126 | def lit(i: Int): F[Int] 127 | } 128 | 129 | object AbstractInterp { 130 | val id: AbstractInterp[Id] = 131 | new AbstractInterp[Id] { 132 | def lit(i: Int): Int = i 133 | } 134 | } 135 | 136 | @fin("ImplOp") 137 | trait ImplInterp[F[_]] { 138 | def foo(i: Int, j: Int): F[Int] 139 | def fooSwapped(i: Int, j: Int): F[Int] = fooSwapped(j, i) 140 | } 141 | 142 | @fin("ParametricOp") 143 | trait ParametricInterp[F[_]] { 144 | def lift[A](a: A): F[A] 145 | } 146 | 147 | @fin("ArityOp") 148 | trait ArityInterp[F[_, _]] { 149 | def pair[A, B](a: A, b: B): F[A, B] 150 | } 151 | 152 | @fin("EffectOp") 153 | trait EffectInterp[F[_]] { 154 | def lit(n: Int): F[Int] 155 | def add(lhs: F[Int], rhs: F[Int]): F[Int] 156 | } 157 | 158 | @fin("NonEffectOp", "NonEffectReader") 159 | trait NonEffectInterp[F[_]] { 160 | def identity[A](a: A): A 161 | def aString: String = NonEffectInterp.s 162 | } 163 | 164 | object NonEffectInterp { 165 | val s = "non-effect op" 166 | val id: NonEffectInterp[Id] = 167 | new NonEffectInterp[Id] { 168 | def identity[A](a: A): A = a 169 | } 170 | } 171 | 172 | @fin(algebraName = "NamedAlgebraOp") 173 | trait NamedAlgebraInterp[F[_]] { 174 | def named(s: String): F[Int] 175 | } 176 | 177 | object NamedAlgebraInterp { 178 | val id: NamedAlgebraInterp[Id] = 179 | new NamedAlgebraInterp[Id] { 180 | def named(s: String): Int = s.size 181 | } 182 | } 183 | 184 | @fin(algebraName = "NamedAuxAlgebraOp1", auxAlgebraName = "NamedAuxAlgebraOp2") 185 | trait NamedAuxAlgebraInterp[F[_]] { 186 | def noop(i: Int): Int 187 | } 188 | 189 | object NamedAuxAlgebraInterp { 190 | val id: NamedAuxAlgebraInterp[Id] = 191 | new NamedAuxAlgebraInterp[Id] { 192 | def noop(i: Int): Int = i 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/Example.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import tfm.{fin, local} 4 | 5 | @fin(algebraName = "MyAlgebra") 6 | trait ExampleInterpreter[F[_]] { 7 | @local def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] 8 | 9 | def lit(n: Int): F[Int] 10 | def add(lhs: Int, rhs: Int): F[Int] 11 | } 12 | 13 | object ExampleInterpreter { 14 | type Id[A] = A 15 | 16 | val id: ExampleInterpreter[Id] = 17 | new ExampleInterpreter[Id] { 18 | def map2[A, B, C](fa: A, fb: B)(f: (A, B) => C): C = 19 | f(fa, fb) 20 | def lit(n: Int): Int = n 21 | def add(lhs: Int, rhs: Int): Int = lhs + rhs 22 | } 23 | } 24 | 25 | object ExampleApp extends App { 26 | // Generated by macro 27 | import ExampleInterpreter.MyAlgebra 28 | import ExampleInterpreter.language._ 29 | 30 | val program1 = add(3, 4) 31 | println(program1.run(ExampleInterpreter.id)) 32 | 33 | // Doesn't exist 34 | // ExampleInterpreter.language.map2 35 | 36 | // Compose primitives 37 | def add3(x: Int, y: Int, z: Int): MyAlgebra[Int] = 38 | new MyAlgebra[Int] { 39 | def run[F[_]](interpreter: ExampleInterpreter[F]): F[Int] = 40 | interpreter.map2(interpreter.add(x, y), interpreter.lit(z))(_ + _) 41 | } 42 | 43 | val program2 = add3(1, 2, 3) 44 | println(program2.run(ExampleInterpreter.id)) 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/Free.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import cats.{Applicative, Id} 4 | import cats.implicits._ 5 | 6 | import tfm.fin 7 | 8 | @fin("FreeApplicative") 9 | trait ApplicativeInterpreter[F[_]] { 10 | def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] 11 | def pure[A](a: A): F[A] 12 | } 13 | 14 | object ApplicativeInterpreter { 15 | def applicative[F[_]](implicit F: Applicative[F]): ApplicativeInterpreter[F] = 16 | new ApplicativeInterpreter[F] { 17 | def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = 18 | F.map2(fa, fb)(f) 19 | def pure[A](a: A): F[A] = F.pure(a) 20 | } 21 | } 22 | 23 | object ApplicativeApp extends App { 24 | import ApplicativeInterpreter.applicative 25 | import ApplicativeInterpreter.language._ 26 | 27 | val program = map2(pure(1), pure(2))(_ + _) 28 | val id = program.run(applicative[Id]) 29 | val list = program.run(applicative[List]) 30 | val option = program.run(applicative[Option]) 31 | 32 | val output = 33 | s""" 34 | Id = ${id} 35 | List = ${list} 36 | Option = ${option} 37 | """ 38 | 39 | println(output) 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/List.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import cats.Monoid 4 | import cats.implicits._ 5 | 6 | import tfm.fin 7 | 8 | @fin(algebraName = "EncodedList") 9 | trait ListInterpreter[F[_]] { 10 | def nil[A]: F[A] 11 | def cons[A](head: A, tail: F[A]): F[A] 12 | } 13 | 14 | object ListInterpreter { 15 | type FoldMap[A] = { type L[X] = (X => A) => A } 16 | 17 | def foldMap[B](implicit B: Monoid[B]): ListInterpreter[FoldMap[B]#L] = 18 | new ListInterpreter[FoldMap[B]#L] { 19 | def nil[A]: (A => B) => B = _ => B.empty 20 | def cons[A](head: A, tail: (A => B) => B): (A => B) => B = 21 | (map: A => B) => B.combine(map(head), tail(map)) 22 | } 23 | 24 | type FoldRight[A] = { type L[X] = A => ((X, A) => A) => A } 25 | 26 | def foldRight[B]: ListInterpreter[FoldRight[B]#L] = 27 | new ListInterpreter[FoldRight[B]#L] { 28 | def nil[A]: B => ((A, B) => B) => B = b => _ => b 29 | def cons[A](head: A, tail: B => ((A, B) => B) => B): B => ((A, B) => B) => B = 30 | (n: B) => (c: (A, B) => B) => c(head, tail(n)(c)) 31 | } 32 | } 33 | 34 | object MonoidApp extends App { 35 | import ListInterpreter._ 36 | import ListInterpreter.language._ 37 | 38 | val program1 = nil[Int] 39 | val result1 = program1.run[FoldMap[Int]#L](foldMap[Int])(identity) // 0 40 | 41 | val program2 = cons(1, cons(2, cons(3, nil))) 42 | val result2 = program2.run[FoldMap[Int]#L](foldMap[Int])(identity) // 6 43 | 44 | val result3 = program2.run[FoldMap[List[Int]]#L](foldMap[List[Int]])(List(_)) // List(1, 2, 3) 45 | 46 | val result4 = program2.run[FoldRight[List[Int]]#L](foldRight[List[Int]])(List.empty)(_ :: _) // List(1, 2, 3) 47 | 48 | // Check if 2 exists in the list 49 | val result5 = program2.run[FoldRight[Boolean]#L](foldRight[Boolean])(false)((x, b) => b || (x == 2)) 50 | 51 | println(s"result1 = ${result1}\nresult2 = ${result2}\nresult3 = ${result3}\nresult4 = ${result4}\nresult5 = ${result5}") 52 | } 53 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/Product.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import tfm.{fin, local} 4 | 5 | @fin("EncodedProduct", "ProductReader") 6 | trait ProductInterpreter[F[_, _]] { 7 | def pair[A, B](a: A, b: B): F[A, B] 8 | 9 | def fst[A, B](pair: F[A, B]): A 10 | 11 | def snd[A, B](pair: F[A, B]): B 12 | } 13 | 14 | object ProductInterpreter { 15 | trait ChurchEncoding[A, B] { 16 | def run[R]: ((A, B) => R) => R 17 | } 18 | 19 | val encoded: ProductInterpreter[ChurchEncoding] = 20 | new ProductInterpreter[ChurchEncoding] { 21 | def pair[A, B](a: A, b: B): ChurchEncoding[A, B] = 22 | new ChurchEncoding[A, B] { 23 | def run[R]: ((A, B) => R) => R = f => f(a, b) 24 | } 25 | 26 | def fst[A, B](pair: ChurchEncoding[A, B]): A = 27 | pair.run[A]((a, _) => a) 28 | 29 | def snd[A, B](pair: ChurchEncoding[A, B]): B = 30 | pair.run[B]((_, b) => b) 31 | } 32 | 33 | 34 | val tuple: ProductInterpreter[Tuple2] = 35 | new ProductInterpreter[Tuple2] { 36 | def pair[A, B](a: A, b: B): (A, B) = (a, b) 37 | 38 | def fst[A, B](pair: (A, B)): A = pair._1 39 | 40 | def snd[A, B](pair: (A, B)): B = pair._2 41 | } 42 | } 43 | 44 | object ProductApp extends App { 45 | import ProductInterpreter._ 46 | import ProductInterpreter.language._ 47 | 48 | val product = pair(1, "hello") 49 | val fproduct: ProductReader[Int] = fst(product) 50 | val sproduct: ProductReader[String] = snd(product) 51 | 52 | val cea = fproduct.run(encoded) 53 | val ceb = sproduct.run(encoded) 54 | 55 | val ta = fproduct.run(tuple) 56 | val tb = sproduct.run(tuple) 57 | 58 | val s = 59 | s""" 60 | cea = ${cea} 61 | ceb = ${ceb} 62 | 63 | ta = ${ta} 64 | tb = ${tb} 65 | """ 66 | 67 | println(s) 68 | } 69 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/Sum.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import cats.data.Xor 4 | 5 | import tfm.{fin, local} 6 | 7 | @fin(algebraName = "EncodedSum", auxAlgebraName = "SumReader") 8 | trait SumInterpreter[F[+_, +_]] { 9 | def left[A, B](a: A): F[A, B] 10 | 11 | def right[A, B](b: B): F[A, B] 12 | 13 | def fold[A, B, X](sum: F[A, B])(l: A => X, r: B => X): X 14 | } 15 | 16 | object SumInterpreter { 17 | trait ChurchEncoding[+A, +B] { 18 | def run[R]: ((A => R), (B => R)) => R 19 | } 20 | 21 | val encoded: SumInterpreter[ChurchEncoding] = 22 | new SumInterpreter[ChurchEncoding] { 23 | def left[A, B](a: A): ChurchEncoding[A, B] = 24 | new ChurchEncoding[A, B] { 25 | def run[R]: ((A => R), (B => R)) => R = 26 | (l, _) => l(a) 27 | } 28 | 29 | def right[A, B](b: B): ChurchEncoding[A, B] = 30 | new ChurchEncoding[A, B] { 31 | def run[R]: ((A => R), (B => R)) => R = 32 | (_, r) => r(b) 33 | } 34 | 35 | def fold[A, B, X](sum: ChurchEncoding[A, B])(l: A => X, r: B => X): X = 36 | sum.run(l, r) 37 | } 38 | 39 | 40 | val xor: SumInterpreter[Xor] = 41 | new SumInterpreter[Xor] { 42 | def left[A, B](a: A): Xor[A, B] = Xor.left(a) 43 | 44 | def right[A, B](b: B): Xor[A, B] = Xor.right(b) 45 | 46 | def fold[A, B, X](sum: Xor[A, B])(l: A => X, r: B => X): X = 47 | sum.fold(l, r) 48 | } 49 | } 50 | 51 | object SumApp extends App { 52 | import SumInterpreter._ 53 | import SumInterpreter.language._ 54 | 55 | def exampleFold[F[+_, +_]](sum: EncodedSum[List[Int], String]): SumReader[Int] = 56 | fold(sum)(_.sum, _.size) 57 | 58 | val l = left[List[Int], String](List(1, 2, 3)) 59 | val r = right[List[Int], String]("hello") 60 | 61 | val lei = exampleFold(l).run(encoded) 62 | val rei = exampleFold(r).run(encoded) 63 | 64 | val lxi = exampleFold(l).run(xor) 65 | val rxi = exampleFold(r).run(xor) 66 | 67 | val s = 68 | s""" 69 | ${l.run(encoded)} 70 | ${r.run(encoded)} 71 | lei = ${lei} 72 | rei = ${rei} 73 | 74 | ${l.run(xor)} 75 | ${r.run(xor)} 76 | lxi = ${lxi} 77 | rxi = ${rxi} 78 | """ 79 | 80 | println(s) 81 | } 82 | -------------------------------------------------------------------------------- /examples/src/main/scala/tfm/examples/Terminal.scala: -------------------------------------------------------------------------------- 1 | package tfm.examples 2 | 3 | import cats.FlatMap 4 | import cats.state.State 5 | import cats.std.function._ 6 | import cats.syntax.flatMap._ 7 | import cats.syntax.functor._ 8 | 9 | import tfm.{fin, local} 10 | 11 | @fin("TerminalIO") 12 | trait TerminalInterpreter[F[_]] { 13 | @local def F: FlatMap[F] 14 | def join[A](lhs: F[A], rhs: F[A]): F[A] = lhs 15 | 16 | // Not valid algebra - `f` has type containing `F[_]`, namely `Function1[A, F[B]]` 17 | // def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = ??? 18 | 19 | val readLine: F[String] 20 | def writeLine(string: String): F[Unit] 21 | } 22 | 23 | object TerminalInterpreter { 24 | val io: TerminalInterpreter[IO] = 25 | new TerminalInterpreter[IO] { 26 | val console = System.console() 27 | 28 | val F: FlatMap[IO] = FlatMap[IO] 29 | val readLine: IO[String] = IO { console.readLine() } 30 | def writeLine(string: String): IO[Unit] = IO { println(string) } 31 | } 32 | 33 | type MockState[A] = State[Mock, A] 34 | val mock: TerminalInterpreter[MockState] = 35 | new TerminalInterpreter[MockState] { 36 | val F: FlatMap[MockState] = FlatMap[MockState] 37 | val readLine: MockState[String] = State(Mock.read) 38 | def writeLine(string: String): MockState[Unit] = State.modify(Mock.write(string)) 39 | } 40 | 41 | implicit val terminalIOFlatMap: FlatMap[TerminalIO] = 42 | new FlatMap[TerminalIO] { 43 | def flatMap[A, B](fa: TerminalIO[A])(f: A => TerminalIO[B]): TerminalIO[B] = 44 | new TerminalIO[B] { 45 | def run[F[_]](interpreter: TerminalInterpreter[F]): F[B] = 46 | interpreter.F.flatMap(fa.run(interpreter))(a => f(a).run(interpreter)) 47 | } 48 | 49 | def map[A, B](fa: TerminalIO[A])(f: A => B): TerminalIO[B] = 50 | new TerminalIO[B] { 51 | def run[F[_]](interpreter: TerminalInterpreter[F]): F[B] = 52 | interpreter.F.map(fa.run(interpreter))(f) 53 | } 54 | } 55 | } 56 | 57 | object TerminalIOApp extends App { 58 | import TerminalInterpreter.language._ 59 | 60 | val program = 61 | for { 62 | _ <- writeLine("Enter something") 63 | x <- readLine 64 | _ <- writeLine("Enter another thing") 65 | y <- readLine 66 | _ <- writeLine(x ++ " " ++ y) 67 | } yield () 68 | 69 | // Mock 70 | val init = Mock(in = List("Hello", "World"), out = List()) 71 | println("Mock: " ++ program.run(TerminalInterpreter.mock).runS(init).run.toString) 72 | 73 | val program2 = join(writeLine("join1"), writeLine("join2")) 74 | val init2 = Mock(List(), List()) 75 | println("Join: " ++ program2.run(TerminalInterpreter.mock).runS(init2).run.toString) 76 | 77 | // Real 78 | program.run(TerminalInterpreter.io).unsafePerformIO() 79 | } 80 | 81 | 82 | 83 | 84 | 85 | trait IO[A] { def unsafePerformIO(): A } 86 | 87 | object IO { 88 | def apply[A](a: => A): IO[A] = new IO[A] { def unsafePerformIO(): A = a } 89 | 90 | implicit val ioMonad: FlatMap[IO] = 91 | new FlatMap[IO] { 92 | def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = 93 | f(fa.unsafePerformIO()) 94 | 95 | def map[A, B](fa: IO[A])(f: A => B): IO[B] = 96 | new IO[B] { def unsafePerformIO(): B = f(fa.unsafePerformIO()) } 97 | } 98 | } 99 | 100 | case class Mock(in: List[String], out: List[String]) 101 | 102 | object Mock { 103 | def read(mock: Mock): (Mock, String) = mock.in match { 104 | case Nil => (mock, "") 105 | case h :: t => (mock.copy(in = t), h) 106 | } 107 | 108 | def write(value: String)(mock: Mock): Mock = 109 | mock.copy(out = value :: mock.out) 110 | } 111 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | --------------------------------------------------------------------------------