Home > Enterprise >  Scala 3: How do you extract the names of elements from a Mirror.Sum as a tuple?
Scala 3: How do you extract the names of elements from a Mirror.Sum as a tuple?

Time:12-21

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
  • Related