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
будет заменен литером строки времени компиляции (если, конечно, вы нашли его в списке доступных идентификаторов, иначе вы получите ошибку времени компиляции).