Skip to content

Add genNarrow#637

Open
nigredo-tori wants to merge 1 commit intosoftwaremill:scala2from
nigredo-tori:627_fix-recursive-instances
Open

Add genNarrow#637
nigredo-tori wants to merge 1 commit intosoftwaremill:scala2from
nigredo-tori:627_fix-recursive-instances

Conversation

@nigredo-tori
Copy link
Copy Markdown

@nigredo-tori nigredo-tori commented Mar 22, 2026

See #627.

It's difficult to narrow recursive instances, especially in Scala 2 where we don't constrain join and split signatures. The most robust solution (although not the most convenient) is to give the users a way to manually specify the desired typeclass.

This PR adds a new macro, genNarrow, that does just that. It reuses most of the implementation of gen, but replaces uses of Typeclass with a specified * -> * HKT.

Comment on lines +50 to +57
* `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]
* }}}
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

Comment on lines +867 to +881
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)

TypeConstructor(typeclass, appliedType(typeclass, _))
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 was previously done in gen.

Comment on lines +893 to +895
s"""expected a * -> * HKT,
|got: $tpe as ${showRaw(tpe)}
|eta-expanded: ${tpe.etaExpand} as ${showRaw(tpe.etaExpand)}"""
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.


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.

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]
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.

The error messages were changed to fully show the required type.

Comment on lines +34 to +35
implicit def genNarrow[Tc[_] <: CollectFields[Any, _], A]: Tc[A] =
macro Magnolia.genNarrow[Tc, A]
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.

@nigredo-tori nigredo-tori force-pushed the 627_fix-recursive-instances branch from 0774aae to e9478f8 Compare March 22, 2026 16:36
@nigredo-tori nigredo-tori force-pushed the 627_fix-recursive-instances branch from e9478f8 to dd19157 Compare March 22, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant