scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

cached derived instance loses refinements

Open bishabosha opened this issue 5 years ago • 9 comments

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

bishabosha avatar Oct 18 '20 19:10 bishabosha

So would I. Has the definition of summon changed?

milessabin avatar Oct 19 '20 09:10 milessabin

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

bishabosha avatar Oct 19 '20 10:10 bishabosha

Oh, I see. Yes, I think that's a bug ... it should preserve the refinement.

milessabin avatar Oct 19 '20 10:10 milessabin

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

bishabosha avatar Jan 13 '21 15:01 bishabosha

Any plans for this? 👀

nkgm avatar Apr 15 '23 00:04 nkgm

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.

Baccata avatar Mar 27 '24 10:03 Baccata

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

bishabosha avatar Mar 27 '24 10:03 bishabosha

Mmm interesting, that may indeed work. Thanks :+1:

Baccata avatar Mar 27 '24 10:03 Baccata

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.

Baccata avatar May 02 '24 10:05 Baccata