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]
+ }
}