Initial commit; major wip

This commit is contained in:
Michael Pilquist 2015-01-16 22:50:31 -05:00
commit c607d1dde6
6 changed files with 344 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

10
LICENSE Normal file
View File

@ -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.

11
build.sbt Normal file
View File

@ -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)

1
project/build.properties Normal file
View File

@ -0,0 +1 @@
sbt.version=0.13.7

View File

@ -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")
}
}
}

View File

@ -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)
}
}
}
}