StringContext и макросы: простой пример

Я пытаюсь достичь расширения StringContext, которое позволит мне написать это:

val tz = zone"Europe/London" //tz is of type java.util.TimeZone

Но с дополнительным предупреждением о том, что он не должен компилироваться, если предоставленная часовая зона недействительна (предполагая, что ее можно определить во время компиляции).

Здесь вспомогательная функция:

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

Я могу создать немакро-реализацию очень легко:

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

Тогда:

scala> zone"UTC"
res1: java.util.TimeZone = sun.util.calendar.ZoneInfo[id="UTC",offset=0,...

До сих пор так хорошо. За исключением того, что это не подрывает компиляцию, если часовой пояс бессмыслен (например, zone"foobar"); код падает во время выполнения. Я хотел бы расширить его до макроса, но, несмотря на чтение docs, я действительно борюсь с деталями (все детали, если быть точным.)

Кто-нибудь может помочь мне начать здесь? Всепоглощающее, все-танцевальное решение должно посмотреть, определяет ли 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?
      }
    }

}

В соответствии с предложением 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?
}

На используемом сайте допустимый 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

Предположительно, я не должен был использовать Literal(Constant( .. )), чтобы заключить его. Что я должен был использовать?


Последний пример - на основе ответа Трэвиса Брауна ниже

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

}

Ответ 1

Это решение "песня и танец", которое обрабатывает интерполяцию часового пояса:

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

Использование:

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
}

Ответ 2

Проблема заключается в том, что вы не можете перетаскивать экземпляр 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))
  }
}

Аргумент reify будет телом сгенерированного метода - в буквальном смысле, а не после какой-либо оценки, за исключением того, что бит tzExpr.slice будет заменен литером строки времени компиляции (если, конечно, вы нашли его в списке доступных идентификаторов, иначе вы получите ошибку времени компиляции).