Могу ли я в 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, но я не уверен, что он будет работать с более новой версией.

Если вы хотите начать с некоторых чтений:

Теперь, перейдя к теме, макросы 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) приводя к ошибке соответствия во время выполнения.

Другие вопросы по тегам