Могу ли я в Scala неявно преобразовывать только определенные литералы в свой пользовательский тип?
В моем приложении я отслеживаю количество кредитов, которые есть у пользователя. Чтобы добавить проверку типа, я использую Credits
класс похож на этот:
case class Credits(val numCredits: Int) extends Ordered[Credits] {
...
}
Предположим, у меня есть функция def accept(creds: Credits): Unit
что я хочу позвонить. Будет ли для меня способ позвонить
process(Credits(100))
process(0)
а не с этим?
process(10)
Т.е. я бы хотел обеспечить неявное преобразование только из буквального 0
и никто другой. Прямо сейчас у меня просто есть val Zero = Credits(0)
в объекте-компаньоне, и я думаю, что это довольно хорошая практика, но в любом случае мне будет интересен ответ, включая другие комментарии, такие как:
- это можно сделать с помощью неявных макросов в 2.10?
- должен
Credits
Скорее расширить AnyVal и не быть case case в 2.10?
3 ответа
Этот вид проверки во время компиляции - хорошая среда для использования макросов, которая будет доступна в 2.10.
Очень умный парень по имени Джейсон Заугг уже реализовал нечто похожее на то, что вам нужно, но это относится к регулярному выражению: проверка времени компиляции Regex.
Возможно, вы захотите взглянуть на его макрокосм, чтобы увидеть, как это делается и как вы можете кодировать свои собственные макросы с той же целью.
https://github.com/retronym/macrocosm
Если вы действительно хотите узнать больше о макросах, во-первых, я бы сказал, что вам нужно быть смелым, потому что на данный момент документации мало, а API, скорее всего, изменится. Jason Zaugg прекрасно работает с 2.10-M3, но я не уверен, что он будет работать с более новой версией.
Если вы хотите начать с некоторых чтений:
Хорошей точкой входа является веб-сайт scalamacros http://scalamacros.org/ и документ SIP https://docs.google.com/document/d/1O879Iz-567FzVb8kw6N5OBpei9dnbW0ZaT7-XNSa6Cs/edit?pli=1
Если у вас есть время, вы также можете прочитать презентацию Юджина Бурмако: http://scalamacros.org/talks/2012-04-28-MetaprogrammingInScala210.pdf
Теперь, перейдя к теме, макросы Scala представляют собой CAT: "Трансформации AST во время компиляции". Абстрактное синтаксическое дерево - это способ, которым компилятор представляет ваш исходный код. Компилятор применяет последовательные преобразования к AST и на последнем этапе он фактически генерирует Java-байт-код.
Давайте теперь посмотрим на код Джейсона Заугга:
def regex(s: String): scala.util.matching.Regex = macro regexImpl
def regexImpl(c: Context)(s: c.Expr[String]): c.Expr[scala.util.matching.Regex] = {
import c.universe._
s.tree match {
case Literal(Constant(string: String)) =>
string.r // just to check
c.reify(s.splice.r)
}
}
Как вы видели, регулярное выражение - это специальная функция, которая принимает строку и возвращает регулярное выражение, вызывая макрос regexImpl.
Функция макроса получает контекст в первых списках параметров, а во втором аргументе перечисляет параметры макроса в форме c.Expr[A] и возвращает c.Expr[B]. Обратите внимание, что c.Expr является типом, зависящим от пути, т.е. это класс, определенный внутри контекста, поэтому, если у вас есть два контекста, следующее недопустимо
val c1: context1.Expr[String] = ...
val c2: context2.Expr[String] = ...
val c3: context1.Expr[String] = context2.Expr[String] // illegal , compile error
Теперь, если вы посмотрите, что происходит в коде:
- На s.tree есть блок совпадений
- Если s.tree является литералом, содержащим константу String, вызывается string.r
Здесь происходит неявное преобразование из строки в StringOps, определенное в Predef.scala, которое автоматически импортируется при компиляции каждый источник scala
implicit def augmentString(x: String): StringOps = new StringOps(x)
StringOps расширяет scala.collection.immutable.StringLike, который содержит:
def r: Regex = new Regex(toString)
Поскольку макросы выполняются во время компиляции, это будет выполняться во время компиляции, и компиляция завершится неудачей, если будет сгенерировано исключение (это поведение при создании регулярного выражения из недопустимой строки регулярного выражения)
Примечание: к сожалению, API очень нестабилен, если вы посмотрите на http://scalamacros.org/documentation/reference.html вы увидите неработающую ссылку на Context.scala. Правильная ссылка: https://github.com/scala/scala/blob/2.10.x/src/reflect/scala/reflect/makro/Context.scala
По сути, вы хотите зависимые типы. Почему Scala поддерживает ограниченную форму зависимых типов в зависимых от пути типах, она не может делать то, что вы просите.
Edmondo1984 была прекрасная идея предложить макросы, но у него есть некоторые ограничения. Поскольку это было довольно легко, я реализовал это:
case class Credits(numCredits: Int)
object Credits {
implicit def toCredits(n: Int): Credits = macro toCreditsImpl
import scala.reflect.makro.Context
def toCreditsImpl(c: Context)(n: c.Expr[Int]): c.Expr[Credits] = {
import c.universe._
n.tree match {
case arg @ Literal(Constant(0)) =>
c.Expr(Apply(Select(Ident("Credits"), newTermName("apply")),
List(arg)))
case _ => c.abort(c.enclosingPosition, "Expected Credits or 0")
}
}
}
Затем я запустил REPL, определил accept
и прошел основную демонстрацию:
scala> def accept(creds: Credits) { println(creds) }
accept: (creds: Credits)Unit
scala> accept(Credits(100))
Credits(100)
scala> accept(0)
Credits(0)
scala> accept(1)
<console>:9: error: Expected Credits or 0
accept(1)
^
Теперь к проблеме:
scala> val x = 0
x: Int = 0
scala> accept(x)
<console>:10: error: Expected Credits or 0
accept(x)
^
Другими словами, я не могу отслеживать свойства значения, присваиваемого идентификаторам, что и позволяет мне делать зависимые типы.
Но все это кажется мне расточительным. Почему вы хотите конвертировать только 0? Кажется, вам нужно значение по умолчанию, и в этом случае самое простое решение - использовать значение по умолчанию:
scala> def accept(creds: Credits = Credits(0)) { println(creds) }
accept: (creds: Credits)Unit
scala> accept(Credits(100))
Credits(100)
scala> accept()
Credits(0)
Использование может использовать неявную частичную функцию:
scala> case class Credits(val numCredits: Int)
defined class Credits
scala> def process(c: Credits) = {}
process: (c: Credits)Unit
scala> implicit def i2c:PartialFunction[Int, Credits] = { case 0 => Credits(0) }
i2c: PartialFunction[Int,Credits]
Позволяет вам
scala> process(Credits(12))
а также
scala> process(0)
Но:
scala> process(12)
scala.MatchError: 12 (of class java.lang.Integer)
at $anonfun$i2c$1.apply(<console>:9)
at $anonfun$i2c$1.apply(<console>:9)
at .<init>(<console>:12)
at .<clinit>(<console>)
at .<init>(<console>:11)
at .<clinit>(<console>)
at $print(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.call(IMain.scala:704)
at scala.tools.nsc.interpreter.IMain$Request$$anonfun$14.apply(IMain.sca
la:920)
at scala.tools.nsc.interpreter.Line$$anonfun$1.apply$mcV$sp(Line.scala:4
3)
at scala.tools.nsc.io.package$$anon$2.run(package.scala:25)
at java.lang.Thread.run(Unknown Source)
Редактировать: Но да, компилятор все еще позволит process(12)
приводя к ошибке соответствия во время выполнения.