scalainlinescala-3match-typesscala-compiletime

How to define a scala.ValueOf for tuples in scala 3?


The ValueOf type function can be used in the contextual parameters list of a method to pick the single inhabitant of singleton type, or reject the implicit resolution if the type argument isn't a singleton.

enum Color { case red, green, blue }
def singleInhabitantOf[T](using  holder: ValueOf[T]): T = holder.value
println(singleInhabitantOf[Color.red.type] == Color.red) // outputs true

But it is limited. It doesn't work with tuples whose component types are all singletons.

singleInhabitantOf[(Color.red.type, Color.blue.type)] // compile error

The error message is: No singleton value available for (Color.red, Color.blue); eligible singleton types for ValueOf synthesis include literals and stable paths.

So I tried to create a version for tuples this way:

import scala.Tuple

type ValuesOf[T <: Tuple] <: Tuple = T match {
    case EmptyTuple => EmptyTuple
    case h *: t => ValueOf[h] *: ValuesOf[t]
}

def singleInhabitantsOf[T<:Tuple](using holder: ValuesOf[T]): Tuple = holder // a tuple mapping is missing here but how to implement it is another question.

singleInhabitantsOf[(Color.red.type, Color.blue.type)] // Copile error

Unfortunately, the compiler complains saying: No given instance of type ValuesOf[(Color.red, Color.blue)] was found for parameter holder. Apparently it looks for a tuple instance in the implicit context instead of synthetizing a tuple with the singletons instances.

However

summonAll[ValuesOf[(Color.red.type, Color.blue.type)]]

compiles and runs fine. So I could rewrite the method using summonAll like this

inline def singleInhabitantsOf2[T <: Tuple]: Tuple = summonAll[ValuesOf[T]] 

But this solution is not useful for my final purpose, for which the check of singletonness should be in the method signature, such that the compiler don't start processing the body if something is wrong.

Edit @DmytroMitin showed me that the previous paragraph is incorrect. It is okay and sometimes unavoidable to involve the body of the given clause in determining when it provides an instance or not.

Any idea on how to define a ValueOf that works with tuples in the context parameters list?


Solution

  • Type classes and match types are two ways to perform type-level calculations in Scala 3 (like type projections and type classes in Scala 2, type families and type classes in Haskell).

    Mixing type classes and match types can be tricky:

    How to prove that `Tuple.Map[H *: T, F] =:= (F[H] *: Tuple.Map[T, F])` in Scala 3

    scala 3 map tuple to futures of tuple types and back

    Scala3 type matching with multiple types


    It seems you want type classes rather than match types

    trait ValuesOf[T <: Tuple]:
      def value: T
    
    object ValuesOf:
      given ValuesOf[EmptyTuple] with
        val value = EmptyTuple
    
      given [h, t <: Tuple](using vh: ValueOf[h], vt: ValuesOf[t]): ValuesOf[h *: t] with
        val value = vh.value *: vt.value
    
    def singleInhabitantsOf[T <: Tuple](using holder: ValuesOf[T]): T = holder.value
    
    singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)
    

    With match types you can do

    type SingleInhabitantsOf[T <: Tuple] <: Tuple = T match
      case EmptyTuple => EmptyTuple
      case h *: t => h *: SingleInhabitantsOf[t]
    
    inline def singleInhabitantsOf0[T <: Tuple]: SingleInhabitantsOf[T] =
      inline erasedValue[T] match
        case _: EmptyTuple => EmptyTuple
        case _: (h *: t) => valueOf[h] *: singleInhabitantsOf0[t]
    
    // to specify return type, not necessary
    inline def singleInhabitantsOf[T <: Tuple]: T = summonFrom {
      case given (SingleInhabitantsOf[T] =:= T) => singleInhabitantsOf0[T]
    }
    
    singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)
    

    Using standard operations on tuples you can do

    type ValuesOf[T <: Tuple] = Tuple.Map[T, ValueOf]
    
    type InvValueOf[V] = V match
      case ValueOf[a] => a
    
    type SingleInhabitantsOf[T <: Tuple] = Tuple.Map[ValuesOf[T], InvValueOf]
    
    // can't express return type
    inline def singleInhabitantsOf0[T <: Tuple] /*: Tuple.Map[? <: ValuesOf[T], InvValueOf]*//*: SingleInhabitantsOf[T]*/ =
      val valuesOf = summonAll[ValuesOf[T]]
      valuesOf.map[InvValueOf]([b] => (y: b) => y match
        case v: ValueOf[a] => v.value
      ): Tuple.Map[valuesOf.type, InvValueOf]
    
    // to specify return type, not necessary
    inline def singleInhabitantsOf[T <: Tuple]: T = summonFrom {
      case _: (SingleInhabitantsOf[T] =:= T) => singleInhabitantsOf0[T]
    }
    
    singleInhabitantsOf[(Color.red.type, Color.blue.type)] // (red,blue)
    

    But this solution is not useful for my final purpose, for which the check of singletonness should be in the method signature, such that the compiler don't start processing the body if something is wrong.

    Sounds like a little weird requirement. You can always "hide" implicit parameters from the left at the right. In Scala 2 you can rewrite

    def foo[A](implicit tc: TC[A]): Unit = ()
    

    as

    def foo[A]: Unit = macro fooImpl[A]
    
    def fooImpl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
      import c.universe._
      c.inferImplicitValue(weakTypeOf[TC[A]])
      q"()"
    }
    

    In Scala 3 you can rewrite

    def foo[A](using TC[A]): Unit = ()
    

    as

    inline def foo[A]: Unit =
      summonInline[TC[A]]
      ()
    

    Maybe you could tell more about your actual problem (maybe starting a new question) and we could look whether it can be solved via match types, summonAll, constValueTuple and other stuff from scala.compiletime.*.


    But I think it isn't a good practice to relay on compilation errors as decider. That is error message unfriendly and hard to understand.

    I do not mean to argue on code styles but error messages depend on a developer

    def foo[A](using TC[A]): Unit = ()
    
    foo[Int] // No given instance of type App.TC[Int] was found for parameter x$1 of method foo in object App
    
    inline def foo[A]: Unit =
      summonInline[TC[A]]
      ()
    
    foo[Int] // No given instance of type App.TC[Int] was found
    
    def foo[A](using @implicitNotFound("No TC!!!") tc: TC[A]): Unit = ()
    
    foo[Int] // No TC!!!
    
    inline def foo[A]: Unit =
      summonFrom {
        case _: TC[A] => ()
        case _ => error("No TC!!!")
      }
    
    foo[Int] // No TC!!!
    

    I do not insist that you should always use implicits "on the right" rather than implicitls "on the left". I just wanted to show you that you have a choice. Surely, implicits "on the left" are more standard. But implicits "on the right" can sometimes be more flexible. For example please see my answer in (already mentioned) scala 3 map tuple to futures of tuple types and back . I'm using there implicits "on the right" nested deeply in the method body. This could be harder to achieve with implicits "on the left".