commit c607d1dde624abe4908f5e1ddaff815372a55178 Author: Michael Pilquist Date: Fri Jan 16 22:50:31 2015 -0500 Initial commit; major wip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e483ede --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2014, Michael Pilquist +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the scodec team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..7a4d474 --- /dev/null +++ b/build.sbt @@ -0,0 +1,11 @@ +organization := "com.github.mpilquist" +name := "simulacrum" + +scalaVersion := "2.11.5" +crossScalaVersions := Seq("2.11.5") + +libraryDependencies ++= Seq( + "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", + "org.scalatest" %% "scalatest" % "2.2.3" % "test" +) +addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..748703f --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.7 diff --git a/src/main/scala/simulacrum/typeclass.scala b/src/main/scala/simulacrum/typeclass.scala new file mode 100644 index 0000000..2295598 --- /dev/null +++ b/src/main/scala/simulacrum/typeclass.scala @@ -0,0 +1,212 @@ +package simulacrum + +import scala.annotation.StaticAnnotation +import scala.language.experimental.macros +import scala.reflect.macros.Context + +class typeclass extends StaticAnnotation { + def macroTransform(annottees: Any*) = macro TypeClassMacros.generateTypeClass +} + +object TypeClassMacros { + def generateTypeClass(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { + import c.universe._ + + def trace(s: => String) = println(s) + + def adaptMethodForProperType(tparamName: Name, method: DefDef): Option[DefDef] = { + // Method should only be adapted if the first parameter in the first parameter list + // matches `tparamName` + val TargetTypeName = tparamName + for { + firstParamList <- method.vparamss.headOption + firstParam <- firstParamList.headOption + Ident(TargetTypeName) <- Option(firstParam.tpt) + } yield { + val paramssWithoutFirst = { + if (firstParamList.tail.isEmpty) method.vparamss.tail + else firstParamList.tail :: method.vparamss.tail + } + + val paramNamess: List[List[Tree]] = { + val original = method.vparamss map { _ map { p => Ident(p.name) } } + original.updated(0, original(0).updated(0, Ident(TermName("self")))) + } + + val rhs = paramNamess.foldLeft(Select(Ident(TermName("typeClass")), method.name): Tree) { (tree, paramNames) => + Apply(tree, paramNames) + } + + val adapted = DefDef(Modifiers(Flag.FINAL), method.name, method.tparams, paramssWithoutFirst, method.tpt, rhs) + adapted + } + } + + def adaptMethodForAppliedType(tparamName: Name, method: DefDef): Option[DefDef] = { + // Method should only be adapted if the first parameter in the first parameter list + // is an F[X] for some (potentially applied) type X + val TargetTypeName = tparamName + for { + firstParamList <- method.vparamss.headOption + firstParam <- firstParamList.headOption + AppliedTypeTree(Ident(TargetTypeName), arg :: Nil) <- Option(firstParam.tpt) + } yield { + arg match { + // If arg == Ident(TypeName("...")), method can be lifted directly + case Ident(simpleArg) => + val paramssFixed = { + val withoutFirst = { + if (firstParamList.tail.isEmpty) method.vparamss.tail + else firstParamList.tail :: method.vparamss.tail + } + withoutFirst map { _ map { param => + // Rewrite all occurrences of simpleArg to $A + val SimpleArg = simpleArg + def rewrite(t: Tree): Tree = t match { + case Ident(SimpleArg) => Ident(TypeName("$A")) + case AppliedTypeTree(x, ys) => AppliedTypeTree(rewrite(x), ys map { y => rewrite(y) }) + // TODO This is woefully incomplete - no attempt is made at rewriting the types of trees that appear in rhs + case other => other + } + ValDef(param.mods, param.name, rewrite(param.tpt), rewrite(param.rhs)) + }} + } + + val paramNamess: List[List[Tree]] = { + val original = method.vparamss map { _ map { p => Ident(p.name) } } + original.updated(0, original(0).updated(0, Ident(TermName("self")))) + } + + val rhs = paramNamess.foldLeft(Select(Ident(TermName("typeClass")), method.name): Tree) { (tree, paramNames) => + Apply(tree, paramNames) + } + + val fixedTParams = method.tparams.filter { _.name != simpleArg } + + val adapted = DefDef(Modifiers(Flag.FINAL), method.name, fixedTParams, paramssFixed, method.tpt, rhs) + adapted + + case AppliedTypeTree(g, a) => + // We need an additional implicit evidence param + // E.g., op[G[_], A](F[G[A]], ...) => F[$A].op[G[_], A](...)(implicit ev $A =:= G[A]) + trace(s"Not adapting ${method.name} - adaptation of methods on shape F[G[X]] not supported") + method + } + } + } + + def adaptMethods(typeClass: ClassDef, tparamName: Name, proper: Boolean): List[DefDef] = { + val typeClassMethods = typeClass.impl.children.collect { + case m: DefDef if !m.mods.hasFlag(Flag.PRIVATE) && !m.mods.hasFlag(Flag.PROTECTED) => m + } + typeClassMethods.flatMap { method => + trace(s"Adapting method as syntax for a $tparamName: ${method.mods} ${method.name}") + val adapted = if (proper) adaptMethodForProperType(tparamName, method) else adaptMethodForAppliedType(tparamName, method) + trace(s"Adapted to: $adapted") + adapted + } + } + + def generateAdapter(typeClass: ClassDef, tparamName: TypeName, proper: Boolean) = { + val adaptedMethods = adaptMethods(typeClass, tparamName, proper) + val adapterBases: List[Tree] = { + typeClass.impl.parents.flatMap { + case AppliedTypeTree(Ident(TypeName(parentTypeClassName)), arg :: Nil) => + Some(AppliedTypeTree(Select(Ident(TermName(parentTypeClassName)), TypeName("Adapter")), arg :: Nil)) + case other => None + } + } + if (proper) { + q"""trait Adapter[${tparamName}] extends ..${adapterBases} { + def typeClass: ${typeClass.name}[${tparamName}] + def self: ${tparamName} + ..$adaptedMethods + + }""" + } else { + q""" + trait Adapter[${tparamName}[_], $$A] { + def typeClass: ${typeClass.name}[${tparamName}] + def self: ${tparamName}[$$A] + ..$adaptedMethods + }""" + } + } + + def generateCompanion(typeClass: ClassDef, tparam: TypeDef, proper: Boolean, comp: Tree) = { + + val summoner = q"def apply[$tparam](implicit instance: ${typeClass.name}[${tparam.name}]): ${typeClass.name}[${tparam.name}] = instance" + + val adapter = generateAdapter(typeClass, tparam.name, proper) + + val adapterConversion = { + if (proper) { + q"implicit def Adapter[A](a: A)(implicit tc: ${typeClass.name}[A]): Adapter[A] = new Adapter[A] { val self = a; val typeClass = tc }" + } else { + q"implicit def Adapter[F[_], A](fa: F[A])(implicit tc: ${typeClass.name}[F]): Adapter[F, A] = new Adapter[F, A] { val self = fa; val typeClass = tc }" + } + } + + val lowerTypeClassName = TermName(typeClass.name.toString.updated(0, typeClass.name.toString.charAt(0).toLower)) + + val adapterConverter = { + if (proper) { + q""" + implicit class AdapterConverter[A](a: A)(implicit tc: ${typeClass.name}[A]) { + def ${lowerTypeClassName}: Adapter[A] = Adapter(a) + } + """ + } else { + q""" + implicit class AdapterConverter[F[_], A](fa: F[A])(implicit tc: ${typeClass.name}[F]) { + def ${lowerTypeClassName}: Adapter[F, A] = Adapter(fa) + } + """ + } + } + + trace(adapterConverter.toString) + + val q"object $name extends ..$bases { ..$body }" = comp + q""" + object $name extends ..$bases { + ..$body + $summoner + $adapter + $adapterConversion + $adapterConverter + } + """ + } + + def modify(typeClass: ClassDef, companion: Option[ModuleDef]) = { + val (tparam, proper) = typeClass.tparams match { + case hd :: Nil => + hd.tparams.size match { + case 0 => (hd, true) + case 1 => (hd, false) + case n => c.abort(typeClass.pos, "@typeclass may only be applied to types that take a single proper type or type constructor") + } + case other => c.abort(typeClass.pos, "@typeclass may only be applied to types that take a single type parameter") + } + + val modifiedTypeClass = typeClass + + val modifiedCompanion = generateCompanion(typeClass, tparam, proper, companion match { + case Some(c) => c + case None => q"object ${typeClass.name.toTermName} {}" + }) + + c.Expr(q""" + $modifiedTypeClass + $modifiedCompanion + """) + } + + annottees.map(_.tree) match { + case (typeClass: ClassDef) :: Nil => modify(typeClass, None) + case (typeClass: ClassDef) :: (companion: ModuleDef) :: Nil => modify(typeClass, Some(companion)) + case other :: Nil => c.abort(other.pos, "@typeclass can only be applied to traits or abstract classes that take 1 type parameter which is either a proper type or a type constructor") + } + } +} diff --git a/src/test/scala/simulacrum/typeclass.scala b/src/test/scala/simulacrum/typeclass.scala new file mode 100644 index 0000000..ca97c4e --- /dev/null +++ b/src/test/scala/simulacrum/typeclass.scala @@ -0,0 +1,109 @@ +package simulacrum + +import org.scalatest.{ WordSpec, Matchers } + +class TypeClassTest extends WordSpec with Matchers { + + "the @typeclass annotation" should { + + "support type classes that are polymorphic over a proper type," which { + @typeclass trait Semigroup[T] { + def append(x: T, y: T): T + def appendCurried(x: T)(y: T): T = append(x, y) + } + + implicit val semigroupInt: Semigroup[Int] = new Semigroup[Int] { + def append(x: Int, y: Int) = x + y + } + + "generates an implicit summoning method in companion" in { + Semigroup[Int] shouldBe semigroupInt + } + + "generates object oriented style forwarding methods" in { + "1 append 2 shouldBe 3" shouldNot compile + import Semigroup.Adapter + 1 append 2 shouldBe 3 + 1 appendCurried 2 shouldBe 3 + } + + "generates object oriented style forwarding methods via converter pattern" in { + "1 append 2 shouldBe 3" shouldNot compile + import Semigroup.AdapterConverter + 1.semigroup append 2 shouldBe 3 + 1.semigroup appendCurried 2 shouldBe 3 + } + + "supports type class inheritance" in { + @typeclass trait Monoid[X] extends Semigroup[X] { + def id: X + } + implicit val monoidInt: Monoid[Int] = new Monoid[Int] { + def append(x: Int, y: Int) = x + y + def id = 0 + } + Monoid[Int].id shouldBe 0 + Monoid[Int].append(1, 2) shouldBe 3 + Monoid.Adapter(1).append(2) shouldBe 3 + } + + "supports pre-existing companions" in { + @typeclass trait Sg[A] { + def op(x: A, y: A): A + } + object Sg { + def foo = 1 + } + implicit val sgInt: Sg[Int] = new Sg[Int] { + def op(x: Int, y: Int) = x + y + } + + Sg[Int].op(1, 2) shouldBe 3 + Sg.foo shouldBe 1 + } + } + + "support type classes that are polymorphic over a type constructor," which { + @typeclass trait Functor[F[_]] { + def map[A, B](fa: F[A])(f: A => B): F[B] + def as[A, B](fa: F[A], b: => B): F[B] = map(fa)(_ => b) + def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f) + def foo[G[_], A](fga: F[G[A]]): G[F[A]] = ??? + } + + implicit val functorList: Functor[List] = new Functor[List] { + def map[A, B](fa: List[A])(f: A => B) = fa.map(f) + } + + "generates an implicit summoning method in companion" in { + Functor[List] shouldBe functorList + } + + "generates object oriented style forwarding methods" in { + "List(1, 2, 3).as(0) shouldBe List(0, 0, 0)" shouldNot compile + import Functor.Adapter + List(1, 2, 3).as(0) shouldBe List(0, 0, 0) + } + + "generates object oriented style forwarding methods via converter pattern" in { + "List(1, 2, 3).functor.as(0) shouldBe List(0, 0, 0)" shouldNot compile + import Functor.AdapterConverter + List(1, 2, 3).functor.as(0) shouldBe List(0, 0, 0) + } + + "supports type class inheritance" in { + @typeclass trait Monad[G[_]] extends Functor[G] { + def pure[A](a: => A): G[A] + def flatMap[A, B](ga: G[A])(f: A => G[B]): G[B] + def map[A, B](ga: G[A])(f: A => B) = flatMap(ga) { a => pure(f(a)) } + } + implicit val monadList: Monad[List] = new Monad[List] { + def pure[A](a: => A) = List(a) + def flatMap[A, B](ga: List[A])(f: A => List[B]): List[B] = ga.flatMap(f) + } + Monad[List].flatMap(List(1, 2))(x => List(x, x)) shouldBe List(1, 1, 2, 2) + Monad.Adapter(List(1, 2)).flatMap { x => List(x, x) } shouldBe List(1, 1, 2, 2) + } + } + } +}