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?
Update
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.
Update 2
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...
CodePudding user response:
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
}
CodePudding user response:
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