diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/shared/src/main/scala/interface.scala | 8 | ||||
-rw-r--r-- | core/shared/src/main/scala/magnolia.scala | 255 |
2 files changed, 169 insertions, 94 deletions
diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala index 193a6f9..5043680 100644 --- a/core/shared/src/main/scala/interface.scala +++ b/core/shared/src/main/scala/interface.scala @@ -102,11 +102,11 @@ trait Param[Typeclass[_], Type] { * @param parametersArray an array of [[Param]] values for this case class * @tparam Typeclass type constructor for the typeclass being derived * @tparam Type generic type of this parameter */ -abstract class CaseClass[Typeclass[_], Type] private[magnolia] ( +abstract class CaseClass[Typeclass[_], Type, ParamType] private[magnolia] ( val typeName: String, val isObject: Boolean, val isValueClass: Boolean, - parametersArray: Array[Param[Typeclass, Type]] + parametersArray: Array[ParamType] ) { /** constructs a new instance of the case class type @@ -122,13 +122,13 @@ abstract class CaseClass[Typeclass[_], Type] private[magnolia] ( * @param makeParam lambda for converting a generic [[Param]] into the value to be used for * this parameter in the construction of a new instance of the case class * @return a new instance of the case class */ - def construct[Return](makeParam: Param[Typeclass, Type] => Return): Type + def construct[Return](makeParam: ParamType => Return): Type /** a sequence of [[Param]] objects representing all of the parameters in the case class * * For efficiency, this sequence is implemented by an `Array`, but upcast to a * [[scala.collection.Seq]] to hide the mutable collection API. */ - def parameters: Seq[Param[Typeclass, Type]] = parametersArray + def parameters: Seq[ParamType] = parametersArray } /** represents a sealed trait and the context required to construct a new typeclass instance diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index f55dfa7..9f3bda8 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -84,41 +84,40 @@ object Magnolia { else q"${tpe.typeSymbol.name.toTermName}" } - val typeDefs = prefixType.baseClasses.flatMap { cls => - cls.asType.toType.decls.filter(_.isType).find(_.name.toString == "Typeclass").map { tpe => - tpe.asType.toType.asSeenFrom(prefixType, cls) + + def getTypeMember(name: String) = { + val typeDefs = prefixType.baseClasses.flatMap { cls => + cls.asType.toType.decls.filter(_.isType).find(_.name.toString == name).map { tpe => + tpe.asType.toType.asSeenFrom(prefixType, cls) + } } - } - val typeConstructorOpt = - typeDefs.headOption.map(_.typeConstructor) + val typeConstructorOpt = typeDefs.headOption.map(_.typeConstructor) - val typeConstructor = typeConstructorOpt.getOrElse { - c.abort(c.enclosingPosition, - "magnolia: the derivation object does not define the Typeclass type constructor") + typeConstructorOpt.getOrElse { + c.abort(c.enclosingPosition, + s"magnolia: the derivation object does not define the $name type constructor") + } } - def checkMethod(termName: String, category: String, expected: String): Unit = { + val typeConstructor = getTypeMember("Typeclass") + + def getMethod(termName: String): Option[MethodSymbol] = { val term = TermName(termName) - val combineClass = c.prefix.tree.tpe.baseClasses - .find { cls => - cls.asType.toType.decl(term) != NoSymbol - } - .getOrElse { - c.abort( - c.enclosingPosition, - s"magnolia: the method `$termName` must be defined on the derivation object to derive typeclasses for $category" - ) - } - val firstParamBlock = combineClass.asType.toType.decl(term).asTerm.asMethod.paramLists.head - if (firstParamBlock.length != 1) - c.abort(c.enclosingPosition, - s"magnolia: the method `combine` should take a single parameter of type $expected") + val cls = c.prefix.tree.tpe.baseClasses.find(_.asType.toType.decl(term) != NoSymbol) + cls.map { c => + c.asType.toType.decl(term).asTerm.asMethod + } } // FIXME: Only run these methods if they're used, particularly `dispatch` - checkMethod("combine", "case classes", "CaseClass[Typeclass, _]") - checkMethod("dispatch", "sealed traits", "SealedTrait[Typeclass, _]") + getMethod("combine").getOrElse(c.abort(c.enclosingPosition, + s"magnolia: the method `dispatch` should take a single parameter of type CaseClass[Typeclass, _]") + ) + + getMethod("dispatch").getOrElse(c.abort(c.enclosingPosition, + s"magnolia: the method `combine` should take a single parameter of type SealedTrait[Typeclass, _]") + ) def findType(key: Type): Option[TermName] = recursionStack(c.enclosingPosition).frames.find(_.genericType == key).map(_.termName(c)) @@ -254,14 +253,14 @@ object Magnolia { def directInferImplicit(genericType: c.Type, typeConstructor: Type): Option[Typeclass] = { val genericTypeName: String = genericType.typeSymbol.name.decodedName.toString.toLowerCase - val assignedName: TermName = TermName(c.freshName(s"${genericTypeName}Typeclass")) - val typeSymbol = genericType.typeSymbol - val classType = if (typeSymbol.isClass) Some(typeSymbol.asClass) else None - val isCaseClass = classType.exists(_.isCaseClass) - val isCaseObject = classType.exists(_.isModuleClass) - val isSealedTrait = classType.exists(_.isSealed) - - val primitives = Set(typeOf[Double], + lazy val assignedName: TermName = TermName(c.freshName(s"${genericTypeName}Typeclass")) + lazy val typeSymbol = genericType.typeSymbol + lazy val classType = if (typeSymbol.isClass) Some(typeSymbol.asClass) else None + lazy val isCaseClass = classType.exists(_.isCaseClass) + lazy val isCaseObject = classType.exists(_.isModuleClass) + lazy val isSealedTrait = classType.exists(_.isSealed) + + lazy val primitives = Set(typeOf[Double], typeOf[Float], typeOf[Short], typeOf[Byte], @@ -271,20 +270,48 @@ object Magnolia { typeOf[Boolean], typeOf[Unit]) - val isValueClass = genericType <:< typeOf[AnyVal] && !primitives.exists(_ =:= genericType) + lazy val isValueClass = genericType <:< typeOf[AnyVal] && !primitives.exists(_ =:= genericType) - val resultType = appliedType(typeConstructor, genericType) + lazy val resultType = appliedType(typeConstructor, genericType) + + lazy val liftedParamType: Tree = getMethod("param").map { sym => + tq"${c.prefix}.ParamType[$genericType, _]" + }.getOrElse(tq"$magnoliaPkg.Param[$typeConstructor, $genericType]") + + lazy val liftedSubtypeType: Tree = getMethod("subtype").map { sym => + tq"${c.prefix}.SubtypeType[$genericType, _]" + }.getOrElse(tq"$magnoliaPkg.Subtype[$typeConstructor, $genericType]") + lazy val caseClassMethod = getMethod("caseClass").map { sym => + q"${c.prefix}.caseClass[$genericType, $liftedParamType]" + }.getOrElse(q"$magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType, $liftedParamType]") + + lazy val caseClassParamNames = getMethod("caseClass").map { sym => + sym.paramLists.head.map(_.name.decodedName.toString) + }.getOrElse(List("name", "isCaseObject", "isValueClass", "parameters", "constructor")) + + lazy val sealedTraitMethod = getMethod("sealedTrait").map { sym => + q"${c.prefix}.sealedTrait[$genericType, $liftedParamType]" + }.getOrElse(q"$magnoliaPkg.Magnolia.sealedTrait[$typeConstructor, $genericType, $liftedParamType]") + + lazy val sealedTraitParamNames = getMethod("sealedTrait").map { sym => + sym.paramLists.head.map(_.name.decodedName.toString) + }.getOrElse(List("name", "subtypes")) + val className = s"${genericType.typeSymbol.owner.fullName}.${genericType.typeSymbol.name.decodedName}" val result = if (isCaseObject) { val obj = companionRef(genericType) - val impl = q""" - ${c.prefix}.combine($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( - $className, true, false, new $scalaPkg.Array(0), _ => $obj) - ) - """ + val parameters = caseClassParamNames.map { + case "name" => q"$className" + case "isCaseObject" => q"true" + case "isValueClass" => q"false" + case "parameters" => q"new $scalaPkg.Array(0)" + case "constructor" => q"(_ => $obj)" + } + + val impl = q"${c.prefix}.combine($caseClassMethod(..$parameters))" Some(Typeclass(genericType, impl)) } else if (isCaseClass || isValueClass) { val caseClassParameters = genericType.decls.collect { @@ -361,33 +388,52 @@ object Magnolia { val assignments = caseParams.zip(defaults).zipWithIndex.map { case ((CaseParam(param, repeated, typeclass, paramType, ref), defaultVal), idx) => - q"""$paramsVal($idx) = $magnoliaPkg.Magnolia.param[$typeConstructor, $genericType, - $paramType]( - ${param.name.decodedName.toString}, $repeated, $ref, $defaultVal, _.${param.name} - )""" + val paramMethod: Tree = getMethod("param").map { sym => + q"${c.prefix}.param" + }.getOrElse(q"$magnoliaPkg.Magnolia.param[$typeConstructor, $genericType, $paramType]") + + val paramNames = getMethod("param").map { sym => + sym.paramLists.head.map(_.name.decodedName.toString) + }.getOrElse(List("name", "repeated", "typeclass", "default", "dereference")) + + val parameters: List[Tree] = paramNames.map { + case "name" => q"${param.name.decodedName.toString}" + case "repeated" => q"$repeated" + case "typeclass" => q"$ref" + case "default" => q"$defaultVal" + case "dereference" => q"_.${param.name}" + case other => + c.abort(c.enclosingPosition, s"magnolia: method 'param' has an unexpected parameter with name '$other'; permitted parameter names: default, dereference, name, repeated, typeclass") + } + q"""$paramsVal($idx) = $paramMethod(..$parameters)""" } + val parameters = caseClassParamNames.map { + case "name" => q"$className" + case "isCaseObject" => q"false" + case "isValueClass" => q"$isValueClass" + case "parameters" => q"$paramsVal" + case "constructor" => q""" + ($fnVal: $liftedParamType => Any) => + new $genericType(..${caseParams.zipWithIndex.map { + case (typeclass, idx) => + val arg = q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]" + if (typeclass.repeated) q"$arg: _*" else arg + }}) + """ + } + + val impl = q"${c.prefix}.combine($caseClassMethod(..$parameters))" Some( Typeclass( genericType, q"""{ ..$preAssignments - val $paramsVal: $scalaPkg.Array[$magnoliaPkg.Param[$typeConstructor, $genericType]] = + val $paramsVal: $scalaPkg.Array[$liftedParamType] = new $scalaPkg.Array(${assignments.length}) ..$assignments - ${c.prefix}.combine($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( - $className, - false, - $isValueClass, - $paramsVal, - ($fnVal: $magnoliaPkg.Param[$typeConstructor, $genericType] => Any) => - new $genericType(..${caseParams.zipWithIndex.map { - case (typeclass, idx) => - val arg = q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]" - if (typeclass.repeated) q"$arg: _*" else arg - }}) - )) + ${c.prefix}.combine($caseClassMethod(..$parameters)) }""" ) ) @@ -422,28 +468,40 @@ object Magnolia { } val assignments = typeclasses.zipWithIndex.map { - case ((typ, typeclass), idx) => - q"""$subtypesVal($idx) = $magnoliaPkg.Magnolia.subtype[$typeConstructor, $genericType, $typ]( - ${s"${typ.typeSymbol.owner.fullName}.${typ.typeSymbol.name.decodedName}"}, - $typeclass, - (t: $genericType) => t.isInstanceOf[$typ], - (t: $genericType) => t.asInstanceOf[$typ] - )""" + case ((subtype, typeclass), idx) => + val subtypeMethod: Tree = getMethod("subtype").map { sym => + q"${c.prefix}.subtype" + }.getOrElse(q"$magnoliaPkg.Magnolia.subtype[$typeConstructor, $genericType, $subtype]") + + val subtypeParamNames = getMethod("subtype").map { sym => + sym.paramLists.head.map(_.name.decodedName.toString) + }.getOrElse(List("name", "typeclass", "isType", "asType")) + + val parameters = subtypeParamNames.map { + case "name" => q"${subtype.typeSymbol.fullName.toString}" + case "typeclass" => q"$typeclass" + case "isType" => q"(t: $genericType) => t.isInstanceOf[$subtype]" + case "asType" => q"(t: $genericType) => t.asInstanceOf[$subtype]" + } + + q"""$subtypesVal($idx) = $subtypeMethod(..$parameters)""" + } + + val parameters = sealedTraitParamNames.map { + case "name" => q"""${s"${genericType.typeSymbol.owner.fullName}.${genericType.typeSymbol.name.decodedName}"}""" + case "subtypes" => q"$subtypesVal: $scalaPkg.Array[$liftedSubtypeType]" } Some { Typeclass( genericType, q"""{ - val $subtypesVal: $scalaPkg.Array[$magnoliaPkg.Subtype[$typeConstructor, $genericType]] = + val $subtypesVal: $scalaPkg.Array[$liftedSubtypeType] = new $scalaPkg.Array(${assignments.size}) ..$assignments - ${c.prefix}.dispatch(new $magnoliaPkg.SealedTrait( - $className, - $subtypesVal: $scalaPkg.Array[$magnoliaPkg.Subtype[$typeConstructor, $genericType]]) - ): $resultType + ${c.prefix}.dispatch($sealedTraitMethod(..$parameters)): $resultType }""" ) } @@ -452,7 +510,7 @@ object Magnolia { result.map { case Typeclass(t, r) => Typeclass(t, q"""{ - def $assignedName: $resultType = $r + lazy val $assignedName: $resultType = $r $assignedName }""") } @@ -505,46 +563,63 @@ object Magnolia { * * This method is intended to be called only from code generated by the Magnolia macro, and * should not be called directly from users' code. */ - def subtype[Tc[_], T, S <: T](name: String, tc: => Tc[S], isType: T => Boolean, asType: T => S) = + def subtype[Tc[_], T, S <: T](name: String, typeclass: => Tc[S], isType: T => Boolean, asType: T => S) = { + lazy val typeclassVal = typeclass new Subtype[Tc, T] { type SType = S def label: String = name - def typeclass: Tc[SType] = tc + def typeclass: Tc[SType] = typeclassVal def cast: PartialFunction[T, SType] = new PartialFunction[T, S] { def isDefinedAt(t: T) = isType(t) def apply(t: T): SType = asType(t) } } + } /** constructs a new [[Param]] instance * * This method is intended to be called only from code generated by the Magnolia macro, and * should not be called directly from users' code. */ def param[Tc[_], T, P](name: String, - isRepeated: Boolean, - typeclassParam: Tc[P], - defaultVal: => Option[P], - deref: T => P) = new Param[Tc, T] { - type PType = P - def label: String = name - def repeated: Boolean = isRepeated - def default: Option[PType] = defaultVal - def typeclass: Tc[PType] = typeclassParam - def dereference(t: T): PType = deref(t) + repeated: Boolean, + typeclass: Tc[P], + default: => Option[P], + dereference: T => P) = { + val typeclassVal = typeclass + val defaultVal = default + val dereferenceVal = dereference + val repeatedVal = repeated + + new Param[Tc, T] { + type PType = P + def label: String = name + def repeated: Boolean = repeatedVal + def default: Option[PType] = defaultVal + def typeclass: Tc[PType] = typeclassVal + def dereference(t: T): PType = dereferenceVal(t) + } } /** constructs a new [[CaseClass]] instance * * This method is intended to be called only from code generated by the Magnolia macro, and * should not be called directly from users' code. */ - def caseClass[Tc[_], T](name: String, - obj: Boolean, - valClass: Boolean, - params: Array[Param[Tc, T]], - constructor: (Param[Tc, T] => Any) => T) = - new CaseClass[Tc, T](name, obj, valClass, params) { - def construct[R](param: Param[Tc, T] => R): T = constructor(param) + def caseClass[Tc[_], T, ParamType](name: String, + isCaseObject: Boolean, + isValueClass: Boolean, + parameters: Array[ParamType], + constructor: (ParamType => Any) => T) = + new CaseClass[Tc, T, ParamType](name, isCaseObject, isValueClass, parameters) { + def construct[R](param: ParamType => R): T = constructor(param) } + + /** constructs a new [[CaseClass]] instance + * + * This method is intended to be called only from code generated by the Magnolia macro, and + * should not be called directly from users' code. */ + def sealedTrait[Tc[_], T, SubType](name: String, + subtypes: Array[Subtype[Tc, T]]): SealedTrait[Tc, T] = + new SealedTrait[Tc, T](name, subtypes) } private[magnolia] case class DirectlyReentrantException() |