Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 74 additions & 20 deletions core/src/main/scala/magnolia1/magnolia.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,28 @@ object Magnolia {
* [[SealedTrait]], like so, <pre> &lt;derivation&gt;.split(&lt;sealedTrait&gt;): Typeclass[T] </pre> so a definition such as, <pre> def
* split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ... </pre> 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]
* }}}
Comment on lines +50 to +57
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This describes the behavior introduced in #628

*
* 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._
Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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"
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
}

Expand Down Expand Up @@ -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)}"""
Comment on lines +885 to +887
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably too verbose, but I feel like the more information we get here the better. The end users probably wouldn't see this error.

)

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")
Expand Down
56 changes: 36 additions & 20 deletions examples/src/main/scala/magnolia1/examples/collectFields.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment on lines +34 to +35
Copy link
Copy Markdown
Author

@nigredo-tori nigredo-tori Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to be able to write

implicit def genNarrow[Out, A]: CollectFields[Out, A] =
      macro Magnolia.genNarrow[CollectFields[Out, *], A]

However, for whatever reason, this seems to lose the information about what Out actually is.

Wrapping the whole thing in another macro (as in the schema example) seems to solve this, but it's a weird magical workaround for what looks like a compiler bug.

}

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)
)
}
}
}
}
}
39 changes: 39 additions & 0 deletions examples/src/main/scala/magnolia1/examples/schema.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package magnolia1.examples.schema

import magnolia1._

// Slimmed down version of schema derivation from the Caliban library.
Copy link
Copy Markdown
Author

@nigredo-tori nigredo-tori Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combines code from here and here. There's some typer weirdness happening there, so I wanted to make sure it actually worked as intended.


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
}
}
Loading
Loading