Skip to content

Commit e9478f8

Browse files
committed
Add genNarrow
See #627
1 parent 9acae4d commit e9478f8

4 files changed

Lines changed: 231 additions & 67 deletions

File tree

core/src/main/scala/magnolia1/magnolia.scala

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,28 @@ object Magnolia {
4646
* [[SealedTrait]], like so, <pre> &lt;derivation&gt;.split(&lt;sealedTrait&gt;): Typeclass[T] </pre> so a definition such as, <pre> def
4747
* split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ... </pre> will suffice, however the qualifications regarding
4848
* additional type parameters and implicit parameters apply equally to `split` as to `join`.
49+
*
50+
* `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
51+
* contrived example, when configured with `type Typeclass[x] = Either[Any, x]` and corresponding `join` and `split`, `gen` may derive an
52+
* `Either[String, T]` if the target type and available instances allow it. To take advantage of it, one has to define `join` and `split`
53+
* in such a way that they propagate narrowed typeclasses as appropriate:
54+
*
55+
* {{{
56+
* def join[L, T](caseClass: CaseClass[Either[L, *], T]): Either[L, T]
57+
* }}}
58+
*
59+
* Note that narrowing is not perfect, and it will fail for (mutually) recursive target types, producing `Typeclass[T]` instead of a more
60+
* specific variant. To handle such cases, please refer to the [[genNarrow]] macro which supports choosing a specific typeclass family member.
4961
*/
50-
def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = Stack.withContext(c) { (stack, depth) =>
62+
def gen[T](c: whitebox.Context)(implicit T: c.WeakTypeTag[T]): c.Tree =
63+
genImpl(c)(TypeConstructor.fromTypeclass(c), T)
64+
65+
/** Like [[gen]], but instead of `Typeclass[T]` this produces `Tc[T]` for the specified `Tc[_]`.
66+
*/
67+
def genNarrow[Tc[_], T](c: whitebox.Context)(implicit Tc: c.WeakTypeTag[Tc[_]], T: c.WeakTypeTag[T]): c.Tree =
68+
genImpl(c)(TypeConstructor.fromTag[Tc](c)(Tc), T)
69+
70+
private def genImpl[T: c.WeakTypeTag](c: whitebox.Context)(typeConstructor: c.Type, T: c.WeakTypeTag[T]): c.Tree = Stack.withContext(c) { (stack, depth) =>
5171
import c.internal._
5272
import c.universe._
5373
import definitions._
@@ -60,19 +80,6 @@ object Magnolia {
6080

6181
val prefixType = c.prefix.tree.tpe
6282
val prefixObject = prefixType.typeSymbol
63-
val prefixName = prefixObject.name.decodedName
64-
65-
val TypeClassNme = TypeName("Typeclass")
66-
val typeDefs = prefixType.baseClasses.flatMap { baseClass =>
67-
baseClass.asType.toType.decls.collectFirst {
68-
case tpe: TypeSymbol if tpe.name == TypeClassNme =>
69-
tpe.toType.asSeenFrom(prefixType, baseClass)
70-
}
71-
}
72-
73-
val typeConstructor = typeDefs.headOption.fold(
74-
error(s"the derivation $prefixObject does not define the Typeclass type constructor")
75-
)(_.typeConstructor)
7683

7784
val searchType = appliedType(typeConstructor, genericType)
7885
val directlyReentrant = stack.top.exists(_.searchType =:= searchType)
@@ -319,14 +326,14 @@ object Magnolia {
319326
else q"val $name = $rhs"
320327
}
321328

322-
def typeclassTree(genericType: Type, typeConstructor: Type, assignedName: TermName): Either[String, Tree] = {
329+
def typeclassTree(genericType: Type, typeConstructor: c.Type, assignedName: TermName): Either[String, Tree] = {
323330
val searchType = appliedType(typeConstructor, genericType)
324331
val deferredRef =
325332
for (methodName <- stack find searchType)
326333
yield DeferredRef(searchType, methodName.decodedName.toString)
327334

328335
deferredRef.fold {
329-
val path = ChainedImplicit(s"$prefixName.Typeclass", genericType.toString)
336+
val path = ChainedImplicit(typeConstructor.toString, genericType.toString)
330337
val frame = stack.Frame(path, searchType, assignedName)
331338
stack.recurse(frame, searchType, shouldCache) {
332339
Option(c.inferImplicitValue(searchType))
@@ -340,17 +347,15 @@ object Magnolia {
340347
else {
341348
val (top, paths) = stack.trace
342349
val missingType = top.fold(searchType)(_.searchType)
343-
val typeClassName = s"${missingType.typeSymbol.name.decodedName}.Typeclass"
344-
val genericType = missingType.typeArgs.head
345350
val trace = paths.mkString(" in ", "\n in ", "\n")
346-
s"could not find $typeClassName for type $genericType\n$trace"
351+
s"could not find $missingType\n$trace"
347352
}
348353
}
349354
}
350355
}(Right(_))
351356
}
352357

353-
def directInferImplicit(genericType: Type, typeConstructor: Type): Option[Tree] = {
358+
def directInferImplicit(genericType: Type, typeConstructor: c.Type): Option[Tree] = {
354359
val genericTypeName = genericType.typeSymbol.name.decodedName.toString.toLowerCase
355360
val assignedName = c.freshName(TermName(s"${genericTypeName}Typeclass")).encodedName.toTermName
356361
val typeSymbol = genericType.typeSymbol
@@ -428,7 +433,7 @@ object Magnolia {
428433
}
429434

430435
val result = if (isRefinedType) {
431-
error(s"could not infer $prefixName.Typeclass for refined type $genericType")
436+
error(s"could not infer $typeConstructor for refined type $genericType")
432437
} else if (isCaseObject) {
433438
val classBody =
434439
if (isReadOnly) List(EmptyTree)
@@ -734,7 +739,7 @@ object Magnolia {
734739
else for (tree <- result) yield c.untypecheck(expandDeferred.transform(tree))
735740

736741
dereferencedResult.getOrElse {
737-
error(s"could not infer $prefixName.Typeclass for type $genericType")
742+
error(s"could not infer $typeConstructor for type $genericType")
738743
}
739744
}
740745

@@ -847,6 +852,55 @@ object Magnolia {
847852

848853
private[Magnolia] final def keepLeft[A](values: Either[A, _]*): List[A] = MagnoliaUtil.keepLeft(values: _*)
849854

855+
private object TypeConstructor {
856+
def fromTypeclass(c: blackbox.Context): c.Type = {
857+
import c.universe._
858+
859+
val prefixType = c.prefix.tree.tpe
860+
val prefixObject = prefixType.typeSymbol
861+
862+
val TypeClassNme = TypeName("Typeclass")
863+
val typeDefs = prefixType.baseClasses.flatMap { baseClass =>
864+
baseClass.asType.toType.decls.collectFirst {
865+
case tpe: TypeSymbol if tpe.name == TypeClassNme =>
866+
tpe.toType.asSeenFrom(prefixType, baseClass)
867+
}
868+
}
869+
val typeclass = typeDefs.headOption.fold(
870+
c.abort(c.enclosingPosition, s"the derivation $prefixObject does not define the Typeclass type constructor")
871+
)(_.typeConstructor)
872+
873+
typeclass
874+
}
875+
876+
def fromTag[F[_]](c: blackbox.Context)(tag: c.WeakTypeTag[F[_]]): c.Type = {
877+
import c.universe._
878+
import c.internal.polyType
879+
880+
val tpe = tag.tpe
881+
882+
def fail() =
883+
c.abort(
884+
c.enclosingPosition,
885+
s"""expected a * -> * HKT,
886+
|got: $tpe as ${showRaw(tpe)}
887+
|eta-expanded: ${tpe.etaExpand} as ${showRaw(tpe.etaExpand)}"""
888+
)
889+
890+
tpe.etaExpand match {
891+
case poly: PolyType if poly.typeParams.nonEmpty =>
892+
val partiallyApplied = polyType(
893+
poly.typeParams.takeRight(1),
894+
poly.resultType.substituteTypes(
895+
poly.typeParams.dropRight(1),
896+
tpe.typeArgs.dropRight(1)
897+
)
898+
)
899+
partiallyApplied
900+
case _ => fail()
901+
}
902+
}
903+
}
850904
}
851905

852906
@compileTimeOnly("magnolia1.Deferred is used for derivation of recursive typeclasses")

examples/src/main/scala/magnolia1/examples/collectFields.scala

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,44 @@ object CollectFields {
1414
case class IntField(int: Int) extends Field
1515
case class StringField(string: String) extends Field
1616

17-
type Typeclass[A] = CollectFields[Any, A]
18-
1917
implicit val int: CollectFields[IntField, Int] = int => Seq(IntField(int))
2018
implicit val string: CollectFields[StringField, String] = string => Seq(StringField(string))
21-
22-
implicit def gen[A]: CollectFields[Any, A] = macro Magnolia.gen[A]
23-
24-
def join[Out, A](caseClass: ReadOnlyCaseClass[CollectFields[Out, *], A]): CollectFields[Out, A] =
25-
new CollectFields[Out, A] {
26-
override def collectFields(a: A) = caseClass.parameters.flatMap { param =>
27-
param.typeclass.collectFields(
28-
param.dereference(a)
29-
)
19+
implicit def seq[Out, A](implicit A: CollectFields[Out, A]): CollectFields[Out, Seq[A]] =
20+
_.flatMap(A.collectFields)
21+
implicit def option[Out, A](implicit A: CollectFields[Out, A]): CollectFields[Out, Option[A]] =
22+
_.fold(Seq.empty[Out])(A.collectFields)
23+
24+
def instance[Out, A](f: A => Seq[Out]): CollectFields[Out, A] = f(_)
25+
def apply[A](implicit A: CollectFields[_, A]): A.type = A
26+
27+
object genDerivation extends Derivation {
28+
type Typeclass[A] = CollectFields[Any, A]
29+
30+
implicit def gen[A]: Typeclass[A] = macro Magnolia.gen[A]
31+
}
32+
33+
object genNarrowDerivation extends Derivation {
34+
implicit def genNarrow[Tc[_] <: CollectFields[Any, _], A]: Tc[A] =
35+
macro Magnolia.genNarrow[Tc, A]
36+
}
37+
38+
protected trait Derivation {
39+
def join[Out, A](caseClass: ReadOnlyCaseClass[CollectFields[Out, *], A]): CollectFields[Out, A] =
40+
new CollectFields[Out, A] {
41+
override def collectFields(a: A) = caseClass.parameters.flatMap { param =>
42+
param.typeclass.collectFields(
43+
param.dereference(a)
44+
)
45+
}
3046
}
31-
}
32-
33-
def split[Out, A](sealedTrait: SealedTrait[CollectFields[Out, *], A]): CollectFields[Out, A] =
34-
new CollectFields[Out, A] {
35-
override def collectFields(a: A) = sealedTrait.split(a) { subtype =>
36-
subtype.typeclass.collectFields(
37-
subtype.cast(a)
38-
)
47+
48+
def split[Out, A](sealedTrait: SealedTrait[CollectFields[Out, *], A]): CollectFields[Out, A] =
49+
new CollectFields[Out, A] {
50+
override def collectFields(a: A) = sealedTrait.split(a) { subtype =>
51+
subtype.typeclass.collectFields(
52+
subtype.cast(a)
53+
)
54+
}
3955
}
40-
}
56+
}
4157
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package magnolia1.examples.schema
2+
3+
import magnolia1._
4+
5+
// Slimmed down version of schema derivation from the Caliban library.
6+
7+
case class Derived[T](schema: T) extends AnyVal
8+
9+
object DerivedMagnolia {
10+
import magnolia1.Magnolia
11+
12+
import scala.reflect.macros.whitebox
13+
14+
def derivedMagnolia[TC[_], A](c: whitebox.Context)(implicit TC: c.WeakTypeTag[TC[_]], A: c.WeakTypeTag[A]): c.Expr[Derived[TC[A]]] = {
15+
val magnoliaTree = c.Expr[TC[A]](Magnolia.genNarrow[TC, A](c))
16+
c.universe.reify(Derived(magnoliaTree.splice))
17+
}
18+
}
19+
20+
sealed trait SchemaDerivation {
21+
def join[R, T](ctx: ReadOnlyCaseClass[Schema[R, *], T]): Schema[R, T] = new Schema[R, T] {}
22+
def split[R, T](ctx: SealedTrait[Schema[R, *], T]): Schema[R, T] = new Schema[R, T] {}
23+
24+
def genNarrow[R, T]: Schema[R, T] = macro Magnolia.genNarrow[Schema[R, *], T]
25+
}
26+
27+
sealed trait Schema[-R, T]
28+
29+
object Schema extends SchemaDerivation {
30+
31+
implicit val StringSchema: Schema[Any, String] = new Schema[Any, String] {}
32+
33+
object auto extends SchemaDerivation {
34+
implicit def genMacro[R, T]: Derived[Schema[R, T]] =
35+
macro DerivedMagnolia.derivedMagnolia[Schema[R, *], T]
36+
37+
def genAll[R0, T](implicit derived: Derived[Schema[R0, T]]): Schema[R0, T] = derived.schema
38+
}
39+
}

0 commit comments

Comments
 (0)