【问题标题】:Using Macro to Make Case Class使用宏制作案例类
【发布时间】:2015-04-29 16:33:41
【问题描述】:

鉴于以下宏(感谢 @TravisBrown 提供此 help):

JetDim.scala

case class JetDim(dimension: Int) {
  require(dimension > 0)
}

object JetDim {
  def validate(dimension: Int): Int = macro JetDimMacro.apply
  def build(dimension: Int): JetDim = JetDim(validate(dimension))
}

JetDimMacro.scala

import reflect.macros.Context

object JetDimMacro {

    sealed trait PosIntCheckResult
    case class LteqZero(x: Int) extends PosIntCheckResult
    case object NotConstant extends PosIntCheckResult

    def apply(c: Context)(dimension: c.Expr[Int]): c.Expr[Int] = {

        import c.universe._

        getInt(c)(dimension) match {
            case Right(_)          => reify { dimension.splice }
            case Left(LteqZero(x)) => c.abort(c.enclosingPosition, s"$x must be > 0.")
            case Left(NotConstant) => reify { dimension.splice }
        }
    }

    def getInt(c: Context)(dimension: c.Expr[Int]): Either[PosIntCheckResult, Int] = {

        import c.universe._

        dimension.tree match {
            case Literal(Constant(x: Int)) => if (x > 0) Right(x) else Left(LteqZero(x))
            case _                         => Left(NotConstant)
        }
    }
}

它适用于 REPL:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim.validate(-55)
<console>:9: error: -55 must be > 0.
              JetDim.validate(-55)
                             ^

scala> JetDim.validate(100)
res1: Int = 100

但是,我想将此编译时检查(通过JetDimMacro)构建到案例类的apply 方法中。

尝试 1

case class JetDim(dimension: Int) {
  require(dimension > 0)
}

object JetDim {
  private def validate(dimension: Int): Int = macro JetDimMacro.apply
  def build(dimension: Int): JetDim = JetDim(validate(dimension))
}

但是失败了:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim.build(-55)
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at spire.math.JetDim.<init>(Jet.scala:21)
  at spire.math.JetDim$.build(Jet.scala:26)
  ... 43 elided

尝试 2

class JetDim(dim: Int) {
  require(dim > 0)

  def dimension: Int = dim
}

object JetDim {
  private def validate(dimension: Int): Int = macro JetDimMacro.apply
  def apply(dimension: Int): JetDim = {
    validate(dimension)
    new JetDim(dimension)
  }
}

然而那也失败了:

scala> import spire.math.JetDim
import spire.math.JetDim

scala> JetDim(555)
res0: spire.math.JetDim = spire.math.JetDim@4b56f205

scala> JetDim(-555)
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:207)
  at spire.math.JetDim.<init>(Jet.scala:21)
  at spire.math.JetDim$.apply(Jet.scala:30)
  ... 43 elided

我想修改JetDimMacro#apply 以返回JetDim 而不是Int。但是,JetDim 存在于 core 项目中,据我所知,它依赖于 macros 项目(JetDimMacro 存在于其中)。

如何使用JetDim 的伴随对象中的validate 方法在编译时检查正整数?

【问题讨论】:

    标签: scala scala-macros


    【解决方案1】:

    问题在于,当我们在apply 中调用validate 时,我们不再处理常量(单例类型)。所以,validate 得到一个非常量的 Int。

    作为替代方案,您可以尝试对正整数使用隐式 witness,然后 JetDim 将其作为构造函数。例如,类似:

    package com.example
    
    case class JetDim(n: PositiveInt)
    
    case class PositiveInt(value: Int) {
      require(value > 0)
    }
    

    然后,我们添加一个来自 Int =&gt; PositiveInt 的隐式(宏)转换来进行检查。

    import scala.language.experimental.macros
    
    import scala.reflect.macros.blackbox.Context
    
    object PositiveInt {
      implicit def wrapConstantInt(n: Int): PositiveInt = macro verifyPositiveInt
    
      def verifyPositiveInt(c: Context)(n: c.Expr[Int]): c.Expr[PositiveInt] = {
        import c.universe._
    
        val tree = n.tree match {
          case Literal(Constant(x: Int)) if x > 0 =>
            q"_root_.com.example.PositiveInt($n)"
          case Literal(Constant(x: Int)) =>
            c.abort(c.enclosingPosition, s"$x <= 0")
          case x =>
            c.abort(c.enclosingPosition, s"cannot verify $x > 0")
        }
        c.Expr(tree)
      }
    }
    

    然后您可以使用JetDim(12),它将通过,或者JetDim(-12),它将失败(宏将 Int 扩展为 PositiveInt)。

    【讨论】:

    • 谢谢。使用这种方法,包含宏 impl 的 PositiveInt 伴随对象将与 JetDim 存在于同一类中,不是吗?我问的原因是我们是否想在其他地方使用PositiveInt
    • @KevinMeredith 是的,它通常在其他地方很有用,所以让它自己存在是有意义的,以便其他方法/类可以使用它。
    猜你喜欢
    • 2015-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-04
    相关资源
    最近更新 更多