diff --git a/core/src/main/scala/magnolia1/magnolia.scala b/core/src/main/scala/magnolia1/magnolia.scala index 87d6bf88..aedb953f 100644 --- a/core/src/main/scala/magnolia1/magnolia.scala +++ b/core/src/main/scala/magnolia1/magnolia.scala @@ -46,8 +46,28 @@ object Magnolia { * [[SealedTrait]], like so,
 <derivation>.split(<sealedTrait>): Typeclass[T] 
so a definition such as,
 def
     * split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ... 
will suffice, however the qualifications regarding * additional type parameters and implicit parameters apply equally to `split` as to `join`. + * + * `gen` will make an effort to keep the resulting value's type as narrow as possible, which can be useful for typeclass families. As a + * contrived example, when configured with `type Typeclass[x] = Either[Any, x]` and corresponding `join` and `split`, `gen` may derive an + * `Either[String, T]` if the target type and available instances allow it. To take advantage of it, one has to define `join` and `split` + * in such a way that they propagate narrowed typeclasses as appropriate: + * + * {{{ + * def join[L, T](caseClass: CaseClass[Either[L, *], T]): Either[L, T] + * }}} + * + * Note that narrowing is not perfect, and it will fail for (mutually) recursive target types, producing `Typeclass[T]` instead of a more + * specific variant. To handle such cases, please refer to the [[genNarrow]] macro which supports choosing a specific typeclass family member. */ - def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = Stack.withContext(c) { (stack, depth) => + def gen[T](c: whitebox.Context)(implicit T: c.WeakTypeTag[T]): c.Tree = + genImpl(c)(TypeConstructor.fromTypeclass(c), T) + + /** Like [[gen]], but instead of `Typeclass[T]` this produces `Tc[T]` for the specified `Tc[_]`. + */ + def genNarrow[Tc[_], T](c: whitebox.Context)(implicit Tc: c.WeakTypeTag[Tc[_]], T: c.WeakTypeTag[T]): c.Tree = + genImpl(c)(TypeConstructor.fromTag[Tc](c)(Tc), T) + + private def genImpl[T: c.WeakTypeTag](c: whitebox.Context)(typeConstructor: c.Type, T: c.WeakTypeTag[T]): c.Tree = Stack.withContext(c) { (stack, depth) => import c.internal._ import c.universe._ import definitions._ @@ -60,19 +80,6 @@ object Magnolia { val prefixType = c.prefix.tree.tpe val prefixObject = prefixType.typeSymbol - val prefixName = prefixObject.name.decodedName - - val TypeClassNme = TypeName("Typeclass") - val typeDefs = prefixType.baseClasses.flatMap { baseClass => - baseClass.asType.toType.decls.collectFirst { - case tpe: TypeSymbol if tpe.name == TypeClassNme => - tpe.toType.asSeenFrom(prefixType, baseClass) - } - } - - val typeConstructor = typeDefs.headOption.fold( - error(s"the derivation $prefixObject does not define the Typeclass type constructor") - )(_.typeConstructor) val searchType = appliedType(typeConstructor, genericType) val directlyReentrant = stack.top.exists(_.searchType =:= searchType) @@ -326,7 +333,7 @@ object Magnolia { yield DeferredRef(searchType, methodName.decodedName.toString) deferredRef.fold { - val path = ChainedImplicit(s"$prefixName.Typeclass", genericType.toString) + val path = ChainedImplicit(typeConstructor.toString, genericType.toString) val frame = stack.Frame(path, searchType, assignedName) stack.recurse(frame, searchType, shouldCache) { Option(c.inferImplicitValue(searchType)) @@ -340,10 +347,8 @@ object Magnolia { else { val (top, paths) = stack.trace val missingType = top.fold(searchType)(_.searchType) - val typeClassName = s"${missingType.typeSymbol.name.decodedName}.Typeclass" - val genericType = missingType.typeArgs.head val trace = paths.mkString(" in ", "\n in ", "\n") - s"could not find $typeClassName for type $genericType\n$trace" + s"could not find $missingType\n$trace" } } } @@ -428,7 +433,7 @@ object Magnolia { } val result = if (isRefinedType) { - error(s"could not infer $prefixName.Typeclass for refined type $genericType") + error(s"could not infer $typeConstructor for refined type $genericType") } else if (isCaseObject) { val classBody = if (isReadOnly) List(EmptyTree) @@ -734,7 +739,7 @@ object Magnolia { else for (tree <- result) yield c.untypecheck(expandDeferred.transform(tree)) dereferencedResult.getOrElse { - error(s"could not infer $prefixName.Typeclass for type $genericType") + error(s"could not infer $typeConstructor for type $genericType") } } @@ -847,6 +852,55 @@ object Magnolia { private[Magnolia] final def keepLeft[A](values: Either[A, _]*): List[A] = MagnoliaUtil.keepLeft(values: _*) + private object TypeConstructor { + def fromTypeclass(c: blackbox.Context): c.Type = { + import c.universe._ + + val prefixType = c.prefix.tree.tpe + val prefixObject = prefixType.typeSymbol + + val TypeClassNme = TypeName("Typeclass") + val typeDefs = prefixType.baseClasses.flatMap { baseClass => + baseClass.asType.toType.decls.collectFirst { + case tpe: TypeSymbol if tpe.name == TypeClassNme => + tpe.toType.asSeenFrom(prefixType, baseClass) + } + } + val typeclass = typeDefs.headOption.fold( + c.abort(c.enclosingPosition, s"the derivation $prefixObject does not define the Typeclass type constructor") + )(_.typeConstructor) + + typeclass + } + + def fromTag[F[_]](c: blackbox.Context)(tag: c.WeakTypeTag[F[_]]): c.Type = { + import c.universe._ + import c.internal.polyType + + val tpe = tag.tpe + + def fail() = + c.abort( + c.enclosingPosition, + s"""expected a * -> * HKT, + |got: $tpe as ${showRaw(tpe)} + |eta-expanded: ${tpe.etaExpand} as ${showRaw(tpe.etaExpand)}""" + ) + + tpe.etaExpand match { + case poly: PolyType if poly.typeParams.nonEmpty => + val partiallyApplied = polyType( + poly.typeParams.takeRight(1), + poly.resultType.substituteTypes( + poly.typeParams.dropRight(1), + tpe.typeArgs.dropRight(1) + ) + ) + partiallyApplied + case _ => fail() + } + } + } } @compileTimeOnly("magnolia1.Deferred is used for derivation of recursive typeclasses") diff --git a/examples/src/main/scala/magnolia1/examples/collectFields.scala b/examples/src/main/scala/magnolia1/examples/collectFields.scala index 107dec06..b981ebd7 100644 --- a/examples/src/main/scala/magnolia1/examples/collectFields.scala +++ b/examples/src/main/scala/magnolia1/examples/collectFields.scala @@ -14,28 +14,44 @@ object CollectFields { case class IntField(int: Int) extends Field case class StringField(string: String) extends Field - type Typeclass[A] = CollectFields[Any, A] - implicit val int: CollectFields[IntField, Int] = int => Seq(IntField(int)) implicit val string: CollectFields[StringField, String] = string => Seq(StringField(string)) - - implicit def gen[A]: CollectFields[Any, A] = macro Magnolia.gen[A] - - def join[Out, A](caseClass: ReadOnlyCaseClass[CollectFields[Out, *], A]): CollectFields[Out, A] = - new CollectFields[Out, A] { - override def collectFields(a: A) = caseClass.parameters.flatMap { param => - param.typeclass.collectFields( - param.dereference(a) - ) + implicit def seq[Out, A](implicit A: CollectFields[Out, A]): CollectFields[Out, Seq[A]] = + _.flatMap(A.collectFields) + implicit def option[Out, A](implicit A: CollectFields[Out, A]): CollectFields[Out, Option[A]] = + _.fold(Seq.empty[Out])(A.collectFields) + + def instance[Out, A](f: A => Seq[Out]): CollectFields[Out, A] = f(_) + def apply[A](implicit A: CollectFields[_, A]): A.type = A + + object genDerivation extends Derivation { + type Typeclass[A] = CollectFields[Any, A] + + implicit def gen[A]: Typeclass[A] = macro Magnolia.gen[A] + } + + object genNarrowDerivation extends Derivation { + implicit def genNarrow[Tc[_] <: CollectFields[Any, _], A]: Tc[A] = + macro Magnolia.genNarrow[Tc, A] + } + + protected trait Derivation { + def join[Out, A](caseClass: ReadOnlyCaseClass[CollectFields[Out, *], A]): CollectFields[Out, A] = + new CollectFields[Out, A] { + override def collectFields(a: A) = caseClass.parameters.flatMap { param => + param.typeclass.collectFields( + param.dereference(a) + ) + } } - } - - def split[Out, A](sealedTrait: SealedTrait[CollectFields[Out, *], A]): CollectFields[Out, A] = - new CollectFields[Out, A] { - override def collectFields(a: A) = sealedTrait.split(a) { subtype => - subtype.typeclass.collectFields( - subtype.cast(a) - ) + + def split[Out, A](sealedTrait: SealedTrait[CollectFields[Out, *], A]): CollectFields[Out, A] = + new CollectFields[Out, A] { + override def collectFields(a: A) = sealedTrait.split(a) { subtype => + subtype.typeclass.collectFields( + subtype.cast(a) + ) + } } - } + } } diff --git a/examples/src/main/scala/magnolia1/examples/schema.scala b/examples/src/main/scala/magnolia1/examples/schema.scala new file mode 100644 index 00000000..192218ed --- /dev/null +++ b/examples/src/main/scala/magnolia1/examples/schema.scala @@ -0,0 +1,39 @@ +package magnolia1.examples.schema + +import magnolia1._ + +// Slimmed down version of schema derivation from the Caliban library. + +case class Derived[T](schema: T) extends AnyVal + +object DerivedMagnolia { + import magnolia1.Magnolia + + import scala.reflect.macros.whitebox + + def derivedMagnolia[TC[_], A](c: whitebox.Context)(implicit TC: c.WeakTypeTag[TC[_]], A: c.WeakTypeTag[A]): c.Expr[Derived[TC[A]]] = { + val magnoliaTree = c.Expr[TC[A]](Magnolia.genNarrow[TC, A](c)) + c.universe.reify(Derived(magnoliaTree.splice)) + } +} + +sealed trait SchemaDerivation { + def join[R, T](ctx: ReadOnlyCaseClass[Schema[R, *], T]): Schema[R, T] = new Schema[R, T] {} + def split[R, T](ctx: SealedTrait[Schema[R, *], T]): Schema[R, T] = new Schema[R, T] {} + + def genNarrow[R, T]: Schema[R, T] = macro Magnolia.genNarrow[Schema[R, *], T] +} + +sealed trait Schema[-R, T] + +object Schema extends SchemaDerivation { + + implicit val StringSchema: Schema[Any, String] = new Schema[Any, String] {} + + object auto extends SchemaDerivation { + implicit def genMacro[R, T]: Derived[Schema[R, T]] = + macro DerivedMagnolia.derivedMagnolia[Schema[R, *], T] + + def genAll[R0, T](implicit derived: Derived[Schema[R0, T]]): Schema[R0, T] = derived.schema + } +} diff --git a/test/src/test/scala/magnolia1/tests/tests.scala b/test/src/test/scala/magnolia1/tests/tests.scala index 959554e3..6d2e320c 100644 --- a/test/src/test/scala/magnolia1/tests/tests.scala +++ b/test/src/test/scala/magnolia1/tests/tests.scala @@ -455,8 +455,8 @@ class Tests extends munit.FunSuite { case class Beta(alpha: Alpha) Show.gen[Beta] """) - assert(error contains """ - |magnolia: could not find Show.Typeclass for type Double + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.Show.Typeclass[Double] | in parameter 'integer' of product type Alpha | in parameter 'alpha' of product type Beta |""".stripMargin) @@ -477,8 +477,8 @@ class Tests extends munit.FunSuite { case class Gamma(unit: Unit) Show.gen[Gamma] """) - assert(error contains """ - |magnolia: could not find Show.Typeclass for type Unit + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.Show.Typeclass[Unit] | in parameter 'unit' of product type Gamma |""".stripMargin) } @@ -490,8 +490,8 @@ class Tests extends munit.FunSuite { implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen } """) - assert(error contains """ - |magnolia: could not find SemiDefault.Typeclass for type magnolia1.tests.ServiceName1 + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.SemiDefault.Typeclass[magnolia1.tests.ServiceName1] | in parameter 'n' of product type LoggingConfig |""".stripMargin) } @@ -503,8 +503,8 @@ class Tests extends munit.FunSuite { implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen } """) - assert(error contains """ - |magnolia: could not find SemiDefault.Typeclass for type magnolia1.tests.ServiceName2 + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.SemiDefault.Typeclass[magnolia1.tests.ServiceName2] | in parameter 'n' of product type LoggingConfig |""".stripMargin) } @@ -516,8 +516,8 @@ class Tests extends munit.FunSuite { implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen } """) - assert(error contains """ - |magnolia: could not find SemiDefault.Typeclass for type Option[String] + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.SemiDefault.Typeclass[Option[String]] | in parameter 'o' of product type LoggingConfig |""".stripMargin) } @@ -552,8 +552,8 @@ class Tests extends munit.FunSuite { // LabelledBox being invariant in L <: String prohibits the derivation for LabelledBox[Int, _] test("can't show a Box with invariant label") { val error = compileErrors("Show.gen[Box[Int]]") - assert(error contains """ - |magnolia: could not find Show.Typeclass for type L + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.Show.Typeclass[L] | in parameter 'label' of product type magnolia1.tests.LabelledBox[Int, _ <: String] | in coproduct type magnolia1.tests.Box[Int] |""".stripMargin) @@ -688,20 +688,20 @@ class Tests extends munit.FunSuite { test("show chained error stack") { val error = compileErrors("Show.gen[(Int, Seq[(Double, String)])]") - assert(error contains """ - |magnolia: could not find Show.Typeclass for type Double + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.Show.Typeclass[Double] | in parameter '_1' of product type (Double, String) - | in chained implicit Show.Typeclass for type Seq[(Double, String)] + | in chained implicit magnolia1.examples.Show.Typeclass for type Seq[(Double, String)] | in parameter '_2' of product type (Int, Seq[(Double, String)]) |""".stripMargin) } test("show chained error stack when leaf instance is missing") { val error = compileErrors("Show.gen[Schedule]") - assert(error contains """ - |magnolia: could not find Show.Typeclass for type java.time.LocalDate + assert(clue(error) contains """ + |magnolia: could not find magnolia1.examples.Show.Typeclass[java.time.LocalDate] | in parameter 'date' of product type magnolia1.tests.Event - | in chained implicit Show.Typeclass for type Seq[magnolia1.tests.Event] + | in chained implicit magnolia1.examples.Show.Typeclass for type Seq[magnolia1.tests.Event] | in parameter 'events' of product type magnolia1.tests.Schedule |""".stripMargin) } @@ -762,7 +762,7 @@ class Tests extends munit.FunSuite { test("disallow coproduct derivations without split method") { val error = compileErrors("WeakHash.gen[Entity]") - assert(error contains "magnolia: the method `split` must be defined on the derivation object WeakHash to derive typeclasses for sealed traits") + assert(clue(error) contains "magnolia: the method `split` must be defined on the derivation object WeakHash to derive typeclasses for sealed traits") } test("equality of Wrapper") { @@ -801,7 +801,7 @@ class Tests extends munit.FunSuite { test("not attempt to derive instances for refined types") { val error = compileErrors("Show.gen[Character]") - assert(error contains "magnolia: could not infer Show.Typeclass for refined type magnolia1.tests.Character.Id") + assert(clue(error) contains "magnolia: could not infer magnolia1.examples.Show.Typeclass for refined type magnolia1.tests.Character.Id") } test("derive instances for types with refined types if implicit provided") { @@ -811,7 +811,7 @@ class Tests extends munit.FunSuite { test("not attempt to derive instances for Java enums") { val error = compileErrors("Show.gen[WeekDay]") - assert(error contains "magnolia: could not infer Show.Typeclass for type magnolia1.tests.WeekDay") + assert(clue(error) contains "magnolia: could not infer magnolia1.examples.Show.Typeclass for type magnolia1.tests.WeekDay") } test("determine subtypes of Exactly[Int]") { @@ -843,7 +843,7 @@ class Tests extends munit.FunSuite { test("no support for arbitrary derivation result type for recursive classes yet") { val error = compileErrors("ExportedTypeclass.gen[Recursive]") assert(error contains """ - |magnolia: could not find ExportedTypeclass.Typeclass for type Seq[magnolia1.tests.Recursive] + |magnolia: could not find magnolia1.examples.ExportedTypeclass.Typeclass[Seq[magnolia1.tests.Recursive]] | in parameter 'children' of product type magnolia1.tests.Recursive |""".stripMargin) } @@ -917,7 +917,7 @@ class Tests extends munit.FunSuite { test("narrow generated instance types for case classes") { case class Foo(a: Int, b: Int) - val instance = CollectFields.gen[Foo] + val instance = CollectFields.genDerivation.gen[Foo] val collected = instance.collectFields(Foo(123, 456)) // Only compiles because the type was narrowed to `CollectFields[IntField, Foo]` val ints = collected.map(_.int) @@ -929,7 +929,7 @@ class Tests extends munit.FunSuite { case class Bar(a: String, b: String) extends Foo case object Baz extends Foo - val instance = CollectFields.gen[Foo] + val instance = CollectFields.genDerivation.gen[Foo] val collected = instance.collectFields(Bar("abc", "def")) // Only compiles because the type was narrowed to `CollectFields[StringField, Foo]` @@ -940,11 +940,66 @@ class Tests extends munit.FunSuite { test("choose least upper bound as instance type") { case class Foo(a: Int, b: String) - val instance = CollectFields.gen[Foo] + val instance = CollectFields.genDerivation.gen[Foo] val collected = instance.collectFields(Foo(123, "abc")) // Only compiles because the type was narrowed to `CollectFields[Field, Foo]` val fields: Seq[CollectFields.Field] = collected assertEquals(fields, Seq(CollectFields.IntField(123), CollectFields.StringField("abc"))) } + + test("narrow generated instance types for recursive structures with genNarrow") { + val instance = CollectFields.genNarrowDerivation.genNarrow[CollectFields[CollectFields.IntField, *], List[Int]] + + val collected = instance.collectFields(List(1, 2, 3)) + val ints = collected.map(_.int) + assertEquals(ints, Seq(1, 2, 3)) + } + + test("support type aliases with genNarrow") { + type F[x] = CollectFields[CollectFields.IntField, x] + val instance = CollectFields.genNarrowDerivation.genNarrow[F, List[Int]] + + val collected = instance.collectFields(List(1, 2, 3)) + val ints = collected.map(_.int) + assertEquals(ints, Seq(1, 2, 3)) + } + + test("narrow generated instance types for mutually recursive structures with genNarrow") { + case class Foo(value: Int, bar: Option[Bar]) + case class Bar(value: Int, foo: Option[Foo]) + + val instance = + CollectFields.genNarrowDerivation.genNarrow[CollectFields[CollectFields.IntField, *], Foo] + val collected = instance.collectFields(Foo(1, Some(Bar(2, Some(Foo(3, None)))))) + val ints = collected.map(_.int) + assertEquals(ints, Seq(1, 2, 3)) + } + + test("produce readable errors in genNarrow") { + val error = compileErrors( + """ + CollectFields.genNarrowDerivation.genNarrow[ + ({ type F[x] = CollectFields[CollectFields.IntField, x] }) # F, + List[String] + ] + """ + ) + assert( + clue(error).contains(""" + |magnolia: could not find magnolia1.examples.CollectFields[magnolia1.examples.CollectFields.IntField,String] + | in parameter 'head' of product type scala.collection.immutable.::[String] + | in coproduct type List[String]""".stripMargin) + ) + } + + test("support Derived pattern with genNarrow") { + final case class Foo(value: String) + final case class Bar(foo: Foo) + final case class Baz(bar: Bar) + + import magnolia1.examples.schema.Schema + import magnolia1.examples.schema.Schema.auto._ + val _: Schema[Any, Baz] = genAll[Any, Baz] + } }