【问题标题】:When calling a scala function with compile-time macro, how to failover smoothly when it causes compilation errors?使用编译时宏调用scala函数时,导致编译错误时如何顺利故障转移?
【发布时间】:2020-06-07 01:24:59
【问题描述】:

假设我打算在 scala 程序中使用单例/文字类型功能,此功能在 scala 2.12 的 shapeless 库中提供(scala 2.13 支持原生文字类型,但我们以 shapeless 为例)

在 shapeless 中,字面量类型表示为 Witness 对象的依赖路径的内部类型,可以从 Scala 字面量/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

第二行导致编译抛出异常:


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

现在,假设我想编写类似 Option[Witness.Lt[Int]] 的东西,如果它是文字,它可以从 Int 转换。在 scala 类型类约定中,我应该这样写:

    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 级别较低,如果 Witness 隐式转换成功,理论上应该被 fromLit 覆盖。不幸的是,当我执行这段代码时,我得到的只是:

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

Witness 隐式转换永远不会发生。我的问题是:

  1. 为什么implicit proof: T => Witness.Lt[Int]不是以下无形宏的成功召唤者?
  implicit def apply[T](t: T): Witness.Lt[T] = macro SingletonTypeMacros.convertImpl
  1. 如何使用类型类和其他 scala 特性来实现这种类型级推导的平滑回退?最好:

    • 不使用宏

    • 如果不可能,不要使用白盒宏

    • 如果也不是不可能,不要使用将被 dotty 丢弃的宏

【问题讨论】:

    标签: scala shapeless scala-macros dependent-type path-dependent-type


    【解决方案1】:

    Shapeless 定义 implicit instance 类型的 Witness.Aux[T]

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

    implicit conversion 从类型TWitness.Lt[T]

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

    隐式实例 Witness.Aux[T] 仅基于类型 T 解析(无论是 T is a singleton type or nor),就像普通类型类的隐式实例一样。但是隐式转换T => Witness.Lt[T] 不像普通的隐式转换。普通的隐式转换是根据要转换的值的类型来解决或不解决的。但是T => Witness.Lt[T] 的解析与否不仅取决于T 的类型,还取决于t 本身的值(无论是t is constant/stable or not)。

    如果你打开scalacOptions ++= Seq("-Ymacro-debug-lite", "-Xlog-implicits"),你会看到

    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
    

    只检查了implicit def apply[T](t: T): Witness.Lt[T](在w 中工作,但在w2 中没有工作)。

    也在

    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].
    

    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].
    

    implicit def apply[T]: Witness.Aux[T]implicit def apply[T](t: T): Witness.Lt[T] 都经过检查,但都没有工作。

    为什么implicit proof: T =&gt; Witness.Lt[Int]不是以下无形宏的成功召唤师?

    编译器处理函数类型A =&gt; B 的隐式与其他类型的隐式不同。它可以将它们视为隐式转换(视图)。但它是否实际上将它们视为转换或只是类型为 A =&gt; B 的隐式实例(与其他类型一样)取决于布尔值 flag isView

    当你这样做时

    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
    

    isViewtrue。但是当你这样做时

    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
    

    或这里

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

    isViewfalse

    在简单的情况下,隐式 A =&gt; B 的存在和从 AB 的隐式转换是相同的

    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
    

    但在我们的例子中不是。有隐式转换3 =&gt; Witness.Lt[3] 但没有这种类型的实例

    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].
    

    所以它检查implicit def apply[T]: Witness.Aux[T],但不检查implicit def apply[T](t: T): Witness.Lt[T]。我没有深入调试隐式解析,但我怀疑在解析隐式之前没有推断出某些类型。

    没有标准方法可以打开isView,以便在解析... def fromLit... (implicit proof: T =&gt; Witness.Lt[Int]) ... 中的proof 时完全模拟隐式转换的行为。如果我们使用c.inferImplicitView 而不是c.inferImplicitValue,我们可以使用宏打开isView

    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)
        }"""
      }
    

    让我们替换

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

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

    我们还要修改

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

    因为它与 fromLit 不明确。原因类似于those。最简单的解决方法是将其替换为

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

    现在两个

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

    Some(我怀疑这不是你想要的)。这是可以理解的。 Random.nextInt(3)Int。我们只根据类型解析MayHaveWitness。并且有隐式转换Int =&gt; Witness.Lt[Int]。所以是Some

    因此,如果我们希望v1 提供Somev2 提供None,那么我们不能仅基于类型来做到这一点。所以使用类型类的方法是行不通的,我们必须使用宏。

    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")
      }
    }
    

    在这里,我们将(implicit proof: T =&gt; Witness.Lt[Int]) 替换为c.inferImplicitView...,我们不仅探索了literal 的类型,还探索了literal 本身。

    现在

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

    v1Somev2None

    如果您制作fromLit blackbox,它仍然可以工作,但会返回MayHaveWitness 而不是MayHaveWitness.SomeMayHaveWitness.None

    【讨论】:

    • 哇,这是很多实验,现在是大粉丝:) 我会等几天再接受它,希望只有黑盒的方法是可能的(否则它会在 Dotty 中丢失)。理论上,由于隐式召唤算法只是一个递归树搜索,应该很容易告诉编译器回溯而不是立即失败
    • @tribbloid 通常隐式解析基于类型。但是3(只是3,甚至不是3.narrow)和Random.nextInt(3)具有相同的类型Int。所以这不仅仅是普通的隐式解析,它是由白色宏干扰类型推断的隐式解析。
    • @tribbloid 同样隐式解析不仅仅是树搜索。它与类型推断交互(双向)。因此,您正在搜索一棵树,其中节点类型没有完全推断出来,并且在解析过程中被推断得更多。所以问题不仅在于回溯,还在于是否应该这样做。
    • 另一个不基于类型gitter.im/fthomas/refined?at=5f883e1c270d004bcfcb219e的隐式转换示例
    猜你喜欢
    • 2021-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多