cached derived instance loses refinements
Minimized code
package example
trait Sealed[A] {
type NumCases <: Int
}
object Sealed {
import quoted._
transparent inline def derived[A]: Sealed[A] = ${ deriveSealed[A] }
def deriveSealed[A: Type](using Quotes): Expr[Sealed[A]] =
import quotes.reflect._
val tpe = TypeRepr.of[A]
val sym = tpe.classSymbol match
case Some(sym) => sym
case _ => report.throwError(s"${tpe.show} is not a class type")
val numCases = ConstantType(IntConstant(sym.children.length)).asType.asInstanceOf[quoted.Type[Int]]
'{
new {
type NumCases = numCases.Underlying
}
}
}
// second source
import example._
enum MyEnum derives Sealed { case A, B, C }
val fromDerived = summon[Sealed[MyEnum]] // : example.Sealed[MyEnum]
val manual = Sealed.derived[MyEnum] // : example.Sealed[MyEnum]{NumCases = 3}
Expectation
I would expect the cached values to remember type refinements, so e.g. the derived Sealed instance can be used in further type class derivation to extract the NumCases type
So would I. Has the definition of summon changed?
we can see what happens in the typer here
final module class MyEnum$() extends AnyRef() { this: MyEnum.type =>
...
given def derived$Sealed: example.Sealed[MyEnum] =
{
final class $anon() extends Object(), example.Sealed[MyEnum] {
type NumCases = (3 : Int)
def numCases: NumCases = 3
}
new $anon():example.Sealed[MyEnum]{NumCases = (3 : Int)}
}
}
Oh, I see. Yes, I think that's a bug ... it should preserve the refinement.
refinements are still dropped even with no inlining:
package example
trait Serial[A] {
type SerialVersionId <: Long
}
object Serial {
def derived[A]: Serial[A] { type SerialVersionId = 12l } = ???
}
case class Foo() derives Serial
def s: Serial[Foo] { type SerialVersionId = 12l } =
Foo.derived$Serial // error
Any plans for this? 👀
I'm having the exact same issue. As a matter of fact, I'm experimenting with your ops-mirror POC @bishabosha (https://github.com/bishabosha/ops-mirror), and that limitation of derives keyword prevents a world of interesting usecases, in particular one that would allow to unify constructs between "direct-style" interfaces and "IO-style" interfaces, which is very much a unification that I am after.
The last discussion of this was that preserving refinements could lead to cyclic references but it's still worth revisiting to be sure. In my Ops-mirror example i address this by deriving a untyped metadata in companion that is expensive to compute, then I have a lightweight wrapper around it (Endpoints) that adds refinements to that derived metadata by reusing the one in the companion
Mmm interesting, that may indeed work. Thanks :+1:
The last discussion of this was that preserving refinements could lead to cyclic references but it's still worth revisiting to be sure
If I may, I think it'd be nice to revisit, or at least get an example of what could go wrong linked to this issue : without it it's hard to understand why it should be the responsibility of the derives keyword to protect against cyclic refs, instead of the responsibility of the author of the thing that is being derived.