When calling a scala function with compile-time macro, how to failover smoothly when it causes compilation errors?

一世执手 提交于 2021-02-13 05:43:30

问题


Assuming that I intend to use the singleton/literal type feature in a scala program, this feature is provided in shapeless library in scala 2.12 (scala 2.13 supports native literal type but let's use shapeless as an example)

In shapeless, literal type is represented as a path-dependent inner type of Witness object, which can be implicitly converted from a scala literal/const:


import com.tribbloids.spike.BaseSpec
import shapeless.Witness

import scala.util.Random

    val w: Witness.Lt[Int] = 3

    val w2: Witness.Lt[Int] = Random.nextInt(3) // this doesn't compile

The second line cause compilation to throw an exception:


[Error] .../WitnessSuite.scala:14: Expression scala.util.Random.nextInt(3) does not evaluate to a constant or a stable reference value
one error found

Now, assuming that I want to write something like Option[Witness.Lt[Int]] that can be converted from an Int if it is a literal or not. In scala type class convention I should write something like this:

    trait MayHaveWitness {

      type Lit
    }

    trait MayHaveWitness_Implicits0 {

      class Some(val w: Witness.Lt[Int]) extends MayHaveWitness {

        type Lit = w.T
      }
      object None extends MayHaveWitness {

        type Lit = Nothing
      }

      implicit def fromNonLit(v: Int): None.type = None
    }

    object MayHaveWitness extends MayHaveWitness_Implicits0 {

      implicit def fromLit[T](literal: T)(implicit proof: T => Witness.Lt[Int]): MayHaveWitness.Some = new Some(literal)
    }

    val v1: MayHaveWitness = 3
    println(v1.getClass)

    val v2: MayHaveWitness = Random.nextInt(3)
    println(v2.getClass)

MayHaveWitness_Implicits0 is of lower level and theoretically should be overshadowed by fromLit if the Witness implicit conversion is successful. Unfortunately when I execute this code all I got was:

class com.tribbloids.spike.shapeless_spike.WitnessSuite$MayHaveWitness_Implicits0$1$None$
class com.tribbloids.spike.shapeless_spike.WitnessSuite$MayHaveWitness_Implicits0$1$None$

The Witness implicit conversion never happens. My questions are:

  1. why implicit proof: T => Witness.Lt[Int] is not a successful summoner of the following shapeless macro?
  implicit def apply[T](t: T): Witness.Lt[T] = macro SingletonTypeMacros.convertImpl
  1. how do I use type classes & other scala features to implement this smooth fallback of type-level deduction? preferrably:

    • NOT using macro

    • If not possible, NOT using whitebox macro

    • If also not impossible, NOT using macro that will be discarded by dotty


回答1:


Shapeless defines implicit instance of type Witness.Aux[T]

implicit def apply[T]: Witness.Aux[T] = macro SingletonTypeMacros.materializeImpl[T]

and implicit conversion from type T to Witness.Lt[T]

implicit def apply[T](t: T): Witness.Lt[T] = macro SingletonTypeMacros.convertImpl

Implicit instance Witness.Aux[T] is resolved or not based on type T only (whether T is a singleton type or nor) like implicit instances of ordinary type classes. But implicit conversion T => Witness.Lt[T] is not like ordinary implicit conversions. Ordinary implicit conversions are resolved or not based on type of a value to be conversed. But T => Witness.Lt[T] is resolved or not based not only on the type T but also on the value t itself (whether t is constant/stable or not).

If you switch on scalacOptions ++= Seq("-Ymacro-debug-lite", "-Xlog-implicits") you'll see that in

val w: Witness.Lt[Int] = 3 //compiles
//Warning:scalac: performing macro expansion shapeless.this.Witness.apply[Int](3) at source-/media/data/Projects/macrosdemo213/core/src/main/scala/App114_2.scala,line-9,offset=205
//Warning:scalac: _root_.shapeless.Witness.mkWitness[Int(3)](3.asInstanceOf[Int(3)])  

val w2: Witness.Lt[Int] = Random.nextInt(3) //doesn't compile
//Warning:scalac: performing macro expansion shapeless.this.Witness.apply[Int](scala.util.Random.nextInt(3)) at source-/media/data/Projects/macrosdemo213/core/src/main/scala/App114_2.scala,line-10,offset=249
//Warning:scalac: macro expansion has failed: Expression scala.util.Random.nextInt(3) does not evaluate to a constant or a stable reference value
//Error: Expression scala.util.Random.nextInt(3) does not evaluate to a constant or a stable reference value

only implicit def apply[T](t: T): Witness.Lt[T] was checked (and worked in w but didn't work in w2).

Also in

val v1: MayHaveWitness = 3 // compiles but gives None
//Warning:scalac: macro expansion is delayed: shapeless.this.Witness.apply[T]
//Warning:scalac: performing macro expansion shapeless.this.Witness.apply[T]
//Warning:scalac: macro expansion has failed: Type argument T is not a singleton type
//Information: shapeless.this.Witness.apply is not a valid implicit value for Int => shapeless.Witness.Lt[Int] because:
//hasMatchingSymbol reported error: polymorphic expression cannot be instantiated to expected type;
// found   : [T]shapeless.Witness.Aux[T]
//    (which expands to)  [T]shapeless.Witness{type T = T}
// required: Int => shapeless.Witness.Lt[Int]
//    (which expands to)  Int => shapeless.Witness{type T <: Int}
//Information: App.this.MayHaveWitness.fromLit is not a valid implicit value for Int(3) => App.MayHaveWitness because:
//No implicit view available from Int => shapeless.Witness.Lt[Int].

and in

val v2: MayHaveWitness = Random.nextInt(3) // compiles but gives None
//Warning:scalac: macro expansion is delayed: shapeless.this.Witness.apply[T]
//Warning:scalac: performing macro expansion shapeless.this.Witness.apply[T]
//Warning:scalac: macro expansion has failed: Type argument T is not a singleton type
//Warning:scalac: performing macro expansion shapeless.this.Witness.apply[T]
//Information: App.this.MayHaveWitness.fromLit is not a valid implicit value for Int => App.MayHaveWitness because:
//No implicit view available from Int => shapeless.Witness.Lt[Int].
//Information: shapeless.this.Witness.apply is not a valid implicit value for Int => shapeless.Witness.Lt[Int] because:
//hasMatchingSymbol reported error: polymorphic expression cannot be instantiated to expected type;
// found   : [T]shapeless.Witness.Aux[T]
//    (which expands to)  [T]shapeless.Witness{type T = T}
// required: Int => shapeless.Witness.Lt[Int]
//    (which expands to)  Int => shapeless.Witness{type T <: Int}
//Information: App.this.MayHaveWitness.fromLit is not a valid implicit value for Int => App.MayHaveWitness because:
//No implicit view available from Int => shapeless.Witness.Lt[Int].

both implicit def apply[T]: Witness.Aux[T] and implicit def apply[T](t: T): Witness.Lt[T] were checked and none of them worked.

why implicit proof: T => Witness.Lt[Int] is not a successful summoner of the following shapeless macro?

Compiler treats implicits of functional types A => B differently than implicits of other types. It can treat them as implicit conversions (views). But whether it actually treats them as conversions or just implicit instances of type A => B (like other types) depends on boolean flag isView.

When you do

val w: Witness.Lt[Int] = 3 //compiles
val w2: Witness.Lt[Int] = Random.nextInt(3) //doesn't compile
val v1: MayHaveWitness = 3 //compiles
val v2: MayHaveWitness = Random.nextInt(3) //compiles

isView is true. But when you do

implicitly[Int => Witness.Lt[Int]] //doesn't compile
implicitly[3 => Witness.Lt[Int]] //doesn't compile
implicitly[Int => MayHaveWitness] //doesn't compile
implicitly[3 => MayHaveWitness] //doesn't compile

or here

implicit def fromLit... (implicit proof: T => Witness.Lt[Int]) ...
                        ______________________________________

isView is false.

In simple cases existence of implicit A => B and implicit conversion from A to B are the same

class A
class B
// implicit val aToB: A => B = null // this one
implicit def aToB(a: A): B = null   // or this one
implicitly[A => B] //compiles
val b: B = new A //compiles

but not in our case. There is implicit conversion 3 => Witness.Lt[3] but not an instance of this type

val w: Witness.Lt[3] = 3.asInstanceOf[3] //compiles

implicitly[3 => Witness.Lt[3]] // doesn't compile
//Information: shapeless.this.Witness.apply is not a valid implicit value for 3 => shapeless.Witness.Lt[3] because:
//hasMatchingSymbol reported error: polymorphic expression cannot be instantiated to expected type;
// found   : [T]shapeless.Witness.Aux[T]
//    (which expands to)  [T]shapeless.Witness{type T = T}
// required: 3 => shapeless.Witness.Lt[3]
//    (which expands to)  3 => shapeless.Witness{type T <: 3}
//Error: No implicit view available from 3 => shapeless.Witness.Lt[3].

so it checks implicit def apply[T]: Witness.Aux[T] but not implicit def apply[T](t: T): Witness.Lt[T]. I didn't debug implicit resolution deeply but I suspect that some type is not inferred before implicit is resolved.

There is no standard way to switch on isView in order to completely emulate behavior of implicit conversion while resolving proof in ... def fromLit... (implicit proof: T => Witness.Lt[Int]) .... We can switch on isView with macros if we use c.inferImplicitView rather than c.inferImplicitValue

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

trait ImplicitView[A, B] {
  def instance: A => B
}
object ImplicitView {
  implicit def mkImplicitView[A, B]: ImplicitView[A, B] = macro mkImplicitViewImpl[A, B]
  def mkImplicitViewImpl[A: c.WeakTypeTag, B: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._

    val tpA = weakTypeOf[A]
    val tpB = weakTypeOf[B]

    val x = TermName(c.freshName("x"))
    val conversion = c.inferImplicitView(tree = EmptyTree, from = tpA, to = tpB, silent = false)

    q"""new ImplicitView[$tpA, $tpB] {
      def instance: $tpA => $tpB = ($x: $tpA) => $conversion($x)
    }"""
  }

Let's replace

implicit def fromLit[T](literal: T)(implicit proof: T => Witness.Lt[Int]): MayHaveWitness.Some = new Some(literal)

with

implicit def fromLit[T](literal: T)(implicit proof: ImplicitView[T, Witness.Lt[Int]]): MayHaveWitness.Some = new Some(proof.instance(literal))

Also we have to modify

implicit def fromNonLit(v: Int): None.type = None

because it's ambiguous with fromLit. Reasons are similar to those. The easiest fix is to replace it with

implicit def fromNonLit[T](v: T): None.type = None

Now both

val v1: MayHaveWitness = 3
println(v1.getClass)

val v2: MayHaveWitness = Random.nextInt(3)
println(v2.getClass)

give Some (I suspect that's not what you wanted). That's understandable. Random.nextInt(3) is Int. And we were resolving MayHaveWitness based only on types. And there is implicit conversion Int => Witness.Lt[Int]. So it's Some.

So it seems if we want v1 to give Some and v2 to give None then we can't do that based only on types. So approach with type classes will not work and we'll have to use macros.

trait MayHaveWitness {
  type Lit
}

object MayHaveWitness  {
  class Some(val w: Witness.Lt[Int]) extends MayHaveWitness {
    type Lit = w.T
  }
  object None extends MayHaveWitness {
    type Lit = Nothing
  }

  implicit def fromLit[T](literal: T): MayHaveWitness = macro fromLitImpl[T]
  def fromLitImpl[T: c.WeakTypeTag](c: whitebox.Context)(literal: c.Tree): c.Tree = {
    import c.universe._
    val conversion = c.inferImplicitView(tree = literal, from = weakTypeOf[T], to = typeOf[Witness.Lt[Int]], silent = false)
    util.Try(c.typecheck(q"new MayHaveWitness.Some($conversion($literal))"))
      .getOrElse(q"MayHaveWitness.None")
  }
}

Here we replaced (implicit proof: T => Witness.Lt[Int]) with c.inferImplicitView... and we explored not only type of literal but also literal itself.

Now in

val v1: MayHaveWitness = 3
println(v1.getClass)

val v2: MayHaveWitness = Random.nextInt(3)
println(v2.getClass)

v1 gves Some and v2 gives None.

If you make fromLit blackbox it will still work but will return MayHaveWitness instead of MayHaveWitness.Some and MayHaveWitness.None.



来源:https://stackoverflow.com/questions/62205940/when-calling-a-scala-function-with-compile-time-macro-how-to-failover-smoothly

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!