I'm trying to make a schema type that can allow you describe Scala types in a generic, fully-typed manner. I have product and coproduct versions of this, and now I'm trying to derive them using Scala 3's mirrors.
The particular challenge I am currently facing is to extract the element names from the MirroredElemLabels type within a Mirror. My understanding is that these types are singleton types and can be converted to their singleton values using scala.compiletime.constValue.
I can confirm that the MirroredElemLabels are what I am expected in the following test case:
sealed trait SuperT
final case class SubT1( int : Int ) extends SuperT
final case class SubT2( str : String ) extends SuperT
val mirror = summon[Mirror.SumOf[SuperT]]
summon[mirror.MirroredElemLabels =:= ("SubT1", "SubT2")]
I should be able to extract the values with the following type class:
import scala.deriving.Mirror
import scala.compiletime.constValue
trait NamesDeriver[ T ] {
type Names <: Tuple
def derive : Names
}
object NamesDeriver {
type Aux[ T, Ns ] = NamesDeriver[ T ] { type Names = Ns }
inline given mirDeriver[ T, ELs <: Tuple ](
using
mir : Mirror.Of[ T ] { type MirroredElemLabels = ELs },
der : NamesDeriver[ ELs ],
) : NamesDeriver[ T ] with {
type Names = der.Names
def derive : der.Names = der.derive
}
given emptyDeriver : NamesDeriver[ EmptyTuple ] with {
type Names = EmptyTuple
def derive : EmptyTuple = EmptyTuple
}
inline given labelsDeriver[ N <: String & Singleton, Tail <: Tuple ](
using
next : NamesDeriver.Aux[ Tail, Tail ],
) : NamesDeriver[ N *: Tail ] with {
type Names = N *: Tail
def derive : N *: Tail = constValue[ N ] *: next.derive
}
def getNames[ T ](
using
nd : NamesDeriver[ T ],
) : nd.Names = nd.derive
}
But this will not compile:
not a constant type: labelsDeriver.this.N; cannot take constValue
Why can't I use constValue here?
I have seen several methods out there, including Mateusz Kubuszok's below, that use inline methods extract label values using either constValue or ValueOf. I have been able to make these work, (a) I need to be able to do so within a type class instance, and (b) I'm curious why my own approach doesn't work!
To be more clear about my use case, the schema type I've come up encodes the subtypes of a coproduct as a tuple of Subtype[T, ST, N <: String & Singleton, S], where T is the supertype's type, ST is the subtype's type, N is the narrow type of the subtype's name, and S is the narrow type of the subtype's own schema. I'd like to be able to derive this tuple using given type class instances.
Thanks to Mateusz's suggestion, I have been able to get the following version to compile...
import scala.deriving.Mirror
import scala.util.NotGiven
import scala.compiletime.{constValue, erasedValue, summonAll, summonInline}
trait Deriver {
type Derived
def derive : Derived
}
trait MirrorNamesDeriver[ T ] extends Deriver { type Derived <: Tuple }
object MirrorNamesDeriver {
type Aux[ T, Ns <: Tuple ] = MirrorNamesDeriver[ T ] {type Derived = Ns}
// def values(t: Tuple): Tuple = t match
// case (h: ValueOf[_]) *: t1 => h.value *: values(t1)
// case EmptyTuple => EmptyTuple
inline given mirDeriver[ T, ElemLabels <: Tuple, NDRes <: Tuple ](
using
mir: Mirror.SumOf[ T ] {type MirroredElemLabels = ElemLabels},
nd: NamesDeriver.Aux[ ElemLabels, ElemLabels ],
): MirrorNamesDeriver.Aux[ T, ElemLabels ] = {
new MirrorNamesDeriver[ T ] {
type Derived = ElemLabels
def derive: ElemLabels = nd.derive
}
}
}
trait NamesDeriver[ R ] extends Deriver
object NamesDeriver {
type Aux[ R, D ] = NamesDeriver[ R ] { type Derived = D }
inline given emptyDeriver : NamesDeriver[ EmptyTuple ] with {
type Derived = EmptyTuple
def derive : EmptyTuple = EmptyTuple
}
inline given labelsDeriver[ N <: (String & Singleton), Tail <: Tuple ](
using
next : NamesDeriver.Aux[ Tail, Tail ],
) : NamesDeriver.Aux[ N *: Tail, N *: Tail ] = {
val derivedValue = constValue[ N ] *: next.derive
new NamesDeriver[ N *: Tail ] {
type Derived = N *: Tail
def derive : N *: Tail = derivedValue
}
}
inline def getNames[ T ](
using
nd : MirrorNamesDeriver[ T ],
) : nd.Derived = nd.derive
}
However, the above fails the following test case:
sealed trait SuperT
final case class SubT1( int : Int ) extends SuperT
final case class SubT2( str : String ) extends SuperT
"NamesDeriver" should "derive names from a coproduct" in {
val nms = NamesDeriver.getNames[ SuperT ]
nms.size shouldBe 2
}
If I add the following evidence to the using parameter list in mirDeriver: ev : NotGiven[ ElemLabels =:= EmptyTuple ], I get the following compilation error:
But no implicit values were found that match type util.NotGiven[? <: Tuple =:= EmptyTuple].
This suggests that the Mirror has and empty tuple for MirroredElemLabels. But again, I was able to confirm for the same test case that I could summon a mirror whose MirroredElemLabels type is ("SubT1", "SubtT2"). Not only that, but in the same compilation error that says there is no such NotGiven instance, it reports a given Mirror instance with:
{
MirroredElemTypes = (NamesDeriverTest.this.SubT1,
NamesDeriverTest.this.SubT2
); MirroredElemLabels = (("SubT1" : String), ("SubT2" : String))
}
What is going on here?? The plot thickens...
When I needed this functionaliy I just wrote a utility to achieve this, which uses ValueOf (this is like Witness from Shapeless but build-in):
// T is m.MirroredElemLabels - tuple of singleton types describing labels
inline def summonLabels[T <: Tuple]: List[String] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[ValueOf[t]].value.asInstanceOf[String] :: summonLabels[ts]
val labels = summonLabels[p.MirroredElemLabels]
But you could probably implement it with less code using something like
// 1. turn type (A, B, ...) into type (ValueOf[A], ValueOf[B], ...)
// (for MirroredElemLabels A, B, ... =:= String)
// 2. for type (ValueOf[A], ValueOf[B], ...) summon List[ValueOf[A | B | ...]]
// (which should be a List[ValueOf[String]] but if Scala
// gets confused about this you can try `.asInstanceOf`)
// 3. turn it into a List[String]
summonAll[Tuple.Map[p.MirroredElemLabels, ValueOf]]
.map(valueOf => valueOf.value.asInstanceOf[String])
EDIT:
Try to rewrite your code to
inline given labelsDeriver[ N <: String & Singleton, Tail <: Tuple ](
using
next : NamesDeriver.Aux[ Tail, Tail ],
) : NamesDeriver[ N *: Tail ] =
// makes sure value is computed before instance is constructed
val precomputed = constValue[ N ] *: next.derive
new NamesDeriver[ N *: Tail ] {
type Names = N *: Tail
// apparently, compiler thinks that you wanted to put
// constValue resolution into new instance's method body
// rather than within macro, which is why it fails
// so try to force it to compute it in compile-time
def derive : N *: Tail = precomputed
}
Ok I figured out a workaround! Rather than parameterize the Mirror's MirroredElemLabels type in order to include NamesDeriver as a second context parameter in mirDeriver, we can just use summonInline to conjure up the NamesDeriver within the inlined given definition:
transparent inline given mirDeriver[ T ](
using
mir: Mirror.SumOf[ T ],
): MirrorNamesDeriver.Aux[ T, mir.MirroredElemLabels ] = {
val namesDeriver = summonInline[ NamesDeriver.Aux[ mir.MirroredElemLabels, mir.MirroredElemLabels ] ]
new MirrorNamesDeriver[ T ] {
type Derived = mir.MirroredElemLabels
def derive: mir.MirroredElemLabels = namesDeriver.derive
}
}
Adding transparent helps my IDE recognize the resulting type, but it doesn't seem to matter for compilation. Here's the results of the test case:
val deriver = summon[MirrorNamesDeriver[ SuperT ]]
summon[deriver.Derived =:= ("SubT1", "SubT2")]
val nms = MirrorNamesDeriver.getNames[ SuperT ]
println(nms.size)
...output:
val deriver: MirrorNamesDeriver[SuperT]{Derived = ("SubT1", "SubT2")} = anon$4@79d56038
val res0: ("SubT1", "SubT2") =:= ("SubT1", "SubT2") = generalized constraint
val nms: ("SubT1", "SubT2") = (SubT1,SubT2)
2
It turns out it's possible to do this just using type classes summoned via context parameters. See https://github.com/lampepfl/dotty/issues/14150#issuecomment-998586254.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With