scalascala-macrosscala-2.13

how to generate fresh singleton literal type in scala using macros


I need to generate random signleton type on every macro invocation in scala 2.13

I tried something like this, but I can't change macro def return type

def randomSingletonInt: Int = macro randomImpl

def randomImpl(c: blackbox.Context): c.Tree = {
  import c.universe.*
  val number = c.freshName().hashCode
  q"$number"
}

I need something like this

val a = randomSingletonInt // a: 42 = 42
val b = randomSingletonInt // b: -112 = -112

How can I achieve that?

My use case is that I want to use it with implicit resolution and type inference

def randomTag[K <: Int & Singleton](implicit tag: K): CustomType[K]

implicit val a = randomSingletonInt
val tagged = randomTag // tagged: CustomType[42]

Solution

  • You can generate 42 of singleton type 42 instead of type Int with a whitebox macro

    def randomSingletonInt: Int = macro randomImpl
    
    def randomImpl(c: whitebox.Context): c.Tree = {
      import c.universe._
      q"42" // or q"42: 42"
    }
    
    randomSingletonInt // 42{Int(42)} // scalacOptions ++= Seq("-Xprint:typer", "-Xprint-types")
    randomSingletonInt: 42 // checking, compiles
    

    In this sense Scala 2 whitebox macros are similar to Scala 3 transparent inline methods

    https://docs.scala-lang.org/overviews/macros/blackbox-whitebox.html#blackbox-and-whitebox-macros

    https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html#transparent-inline-methods-1

    The problem is that if you assign a value of a singleton type to a variable then this doesn't mean that the type of the variable is singleton

    val a = randomSingletonInt // val a: Int = 42{Int(42)}
    a: 42 // doesn't compile
    

    Solution is to add extra { } (i.e. empty type refinement, this makes impact on type inference)

    def randomImpl(c: whitebox.Context): c.Tree = {
      import c.universe._
      q"42: 42 {}"
    }
    
    val a = randomSingletonInt // val a: 42 = (42{Int(42)}: 42){42}
    a: 42 // compiles
    

    This is what Shapeless .narrow does

    import shapeless.syntax.singleton._
    val a = 42.narrow
    // val a: Int(42) = SingletonOps.instance{[T0](w: shapeless.Witness.Aux[T0]): shapeless.syntax.SingletonOps{type T = T0; val witness: w.type}}[Int(42)]{(w: shapeless.Witness.Aux[Int(42)]): shapeless.syntax.SingletonOps{type T = Int(42); val witness: w.type}}(Witness.mkWitness{[T0](value0: T0): shapeless.Witness.Aux[T0]}[Int(42)]{(value0: Int(42)): shapeless.Witness.Aux[Int(42)]}(42{Int(42)}.asInstanceOf{[T0]T0}[Int(42)]{42}){shapeless.Witness.Aux[Int(42)]}){shapeless.syntax.SingletonOps{type T = Int(42); val witness: shapeless.Witness.Aux[Int(42)]}}.narrow{Int(42)}
    

    https://github.com/milessabin/shapeless/blob/main/core/shared/src/main/scala/shapeless/syntax/singletons.scala#L33

    def narrow: T {} = witness.value
    //           ^^^^
    

    Next problem is that this not always will work further on (with implicits for example)

    val x: 42 = 42
    implicitly[x.type =:= 42] // compiles
    val a = randomSingletonInt
    implicitly[a.type =:= 42] // doesn't compile
    
    trait TC[I <: Int with Singleton]
    object TC {
      implicit val tc: TC[42] = null
    }
    
    implicitly[TC[42]] // compiles
    implicitly[TC[x.type]] // compiles
    implicitly[TC[a.type]] // doesn't compile
    

    So you should better add code you want to compile with a, b and we'll see whether this is possible.

    For example in implicits a type can be cleaned from refinements before comparing with =:=

    trait TC[A <: Int with Singleton]
    object TC {
      implicit def tc[A <: Int with Singleton]: TC[A] = macro tcImpl[A]
    
      def tcImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
        import c.universe._
    
        val typeA = weakTypeOf[A]
    
        def unrefy(tpe: Type): Type = tpe.typeSymbol.typeSignature match {
          case RefinedType(List(tp, _*), _) => tp
          case _ => tpe
        }
    
        val type42 = c.internal.constantType(Constant(42))
    
        if (typeA =:= type42 || unrefy(typeA) =:= type42)
          q"new TC[$typeA] {}"
        else c.abort(c.enclosingPosition, "not 42")
      }
    }
    
    implicitly[TC[42]] // compiles
    implicitly[TC[x.type]] // compiles
    implicitly[TC[a.type]] // compiles
    

    Also solution can be to generate a singleton type rather than a value of singleton type.