StringContext和宏:一个简单的例子

oxb*_*kes 10 scala scala-macros

我正在尝试实现一个StringContext扩展,这将允许我写这个:

val tz = zone"Europe/London" //tz is of type java.util.TimeZone
Run Code Online (Sandbox Code Playgroud)

但有一点需要注意,如果提供的时区无效(假设可以在编译时确定),它将无法编译.

这是一个辅助函数:

def maybeTZ(s: String): Option[java.util.TimeZone] =
  java.util.TimeZone.getAvailableIDs collectFirst { case id if id == s =>
    java.util.TimeZone.getTimeZone(id)
  }
Run Code Online (Sandbox Code Playgroud)

我可以很容易地创建一个非宏实现:

scala> implicit class TZContext(val sc: StringContext) extends AnyVal {
 |   def zone(args: Any *) = {
 |     val s = sc.raw(args.toSeq : _ *)
 |     maybeTZ(s) getOrElse sys.error(s"Invalid zone: $s")
 |   }
 | }
Run Code Online (Sandbox Code Playgroud)

然后:

scala> zone"UTC"
res1: java.util.TimeZone = sun.util.calendar.ZoneInfo[id="UTC",offset=0,...
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.如果时区是无意义的(例如zone"foobar"),那么编译不会失败; 代码在运行时失效.我想将它扩展到一个宏,但是,尽管阅读了文档,但我真的很挣扎于细节(确切地说,所有的细节.)

任何人都可以帮我开始吗?全唱,全舞的解决方案应该看看是否StringContext定义任何参数和(如果是),将计算推迟到运行时,否则尝试在编译时解析区域


我试过了什么?

好吧,宏定义似乎必须在静态可访问的对象中.所以:

package object oxbow {
  implicit class TZContext(val sc: StringContext) extends AnyVal {
    def zone(args: Any *) = macro zoneImpl //zoneImpl cannot be in TZContext
  }

  def zoneImpl(c: reflect.macros.Context)
    (args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
      import c.universe._
      //1. How can I access sc from here?

      /// ... if I could, would this be right?
      if (args.isEmpty) {
        val s = sc.raw()
        reify(maybeTZ(s) getOrElse sys.error(s"Not valid $s")) 
      }
      else {
        //Ok, now I'm stuck. What goes here?
      }
    }

}
Run Code Online (Sandbox Code Playgroud)

根据som-snytt的建议,这是最新的尝试:

def zoneImpl(c: reflect.macros.Context)
           (args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
  import c.universe._
  val z =
    c.prefix.tree match {
      case Apply(_, List(Apply(_, List(Literal(Constant(const: String)))))) => gsa.shared.datetime.XTimeZone.getTimeZone(const)
      case x => ??? //not sure what to put here
    }

  c.Expr[java.util.TimeZone](Literal(Constant(z))) //this compiles but doesn't work at the use-site
                             ^^^^^^^^^^^^^^^^^^^
                             this is wrong. What should it be?
}
Run Code Online (Sandbox Code Playgroud)

在use-site,有效的zone"UTC"编译失败,错误:

java.lang.Error: bad constant value: sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null] of class class sun.util.calendar.ZoneInfo
Run Code Online (Sandbox Code Playgroud)

据推测,我不应该用a Literal(Constant( .. ))来封闭它.我该怎么用?


最后一个例子 - 基于Travis Brown的答案如下

def zoneImpl(c: reflect.macros.Context)
         (args: c.Expr[Any] *): c.Expr[java.util.TimeZone] = {
  import c.universe._
  import java.util.TimeZone

  val tzExpr: c.Expr[String] = c.prefix.tree match {
    case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil)
      if TimeZone.getAvailableIDs contains s => c.Expr(tz)
    case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil) =>
      c.abort(c.enclosingPosition, s"Invalid time zone! $s")
    case _ => ??? 
//            ^^^ What do I do here? I do not want to abort, I merely wish to 
//                "carry on as you were". I've tried ... 
//                    c.prefix.tree.asInstanceOf[c.Expr[String]]
//                ...but that does not work
  }
  c.universe.reify(TimeZone.getTimeZone(tzExpr.splice))

}
Run Code Online (Sandbox Code Playgroud)

som*_*ytt 7

这是处理时区插值的"歌舞"解决方案:

package object timezone {
  import scala.language.implicitConversions
  implicit def zoned(sc: StringContext) = new ZoneContext(sc)
}

package timezone {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context
  import java.util.TimeZone

  class ZoneContext(sc: StringContext) {

    def tz(args: Any*): TimeZone = macro TimeZoned.tzImpl

    // invoked if runtime interpolation is required
    def tz0(args: Any*): TimeZone = {
      val s = sc.s(args: _*)
      val z = TimeZoned maybeTZ s getOrElse (throw new RuntimeException(s"Bad timezone $s"))
      TimeZone getTimeZone z
    }
  }
  object TimeZoned {
    def maybeTZ(s: String): Option[String] =
      if (TimeZone.getAvailableIDs contains s) Some(s) else None

    def tzImpl(c: Context)(args: c.Expr[Any]*): c.Expr[TimeZone] = {
      import c.universe._
      c.prefix.tree match {
        case Apply(_, List(Apply(_, List(tz @Literal(Constant(const: String)))))) =>
          maybeTZ(const) map (
            k => reify(TimeZone getTimeZone c.Expr[String](tz).splice)
          ) getOrElse c.abort(c.enclosingPosition, s"Bad timezone $const")
        case x =>
          val rts = x.tpe.declaration(newTermName("tz0"))
          val rt = treeBuild.mkAttributedSelect(x, rts)
          c.Expr[TimeZone](Apply(rt, args.map(_.tree).toList))
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

用法:

package tztest 

import timezone._

object Test extends App {

  val delta = 8
  //Console println tz"etc/GMT+$delta"  //java.lang.RuntimeException: Bad timezone etc/GMT+8
  Console println tz"Etc/GMT+$delta"
  Console println tz"US/Hawaii"
  //Console println tz"US/Nowayi"     //error: Bad timezone US/Nowayi
}
Run Code Online (Sandbox Code Playgroud)


Tra*_*own 7

问题是你不能将编译时实例走私TimeZone到宏生成的代码中.但是,您可以滑动字符串文字,因此您可以生成将在运行时构建所需的代码TimeZone,同时仍在编译时检查以确保标识符可用.

以下是一个完整的工作示例:

object TimeZoneLiterals {
  import java.util.TimeZone
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  implicit class TZContext(val sc: StringContext) extends AnyVal {
    def zone() = macro zoneImpl
  }

  def zoneImpl(c: reflect.macros.Context)() = {
    import c.universe._

    val tzExpr = c.prefix.tree match {
      case Apply(_, Apply(_, List(tz @ Literal(Constant(s: String)))) :: Nil)
        if TimeZone.getAvailableIDs contains s => c.Expr(tz)
      case _ => c.abort(c.enclosingPosition, "Invalid time zone!")
    }

    reify(TimeZone.getTimeZone(tzExpr.splice))
  }
}
Run Code Online (Sandbox Code Playgroud)

参数reify将是生成方法的主体 - 字面上,不是在任何类型的评估之后,除了该tzExpr.slice位将被编译时字符串文字替换(当然,如果你在列表中找到它)可用标识符 - 否则会出现编译时错误).