Класс значений Scala, поддерживающий сравнения и математические операции

#scala

#scala

Вопрос:

Какой idomatic способ создать класс значений в Scala, который поддерживает сравнения и математические операции? Предположим, у меня есть следующий класс значений…

 case class Price(value: Double) extends AnyVal
  

Я хотел бы иметь возможность делать такие вещи, как…

 val price1 = Price(23.4)
val price2 = Price(1.0)

price1 <= price2
price1   price2
  

…etc для всех других операторов сравнения и математических операций. Одним из решений является реализация каждого из желаемых методов вручную…

 case class Price(value: Double) extends AnyVal {

   def <=(that: Price): Boolean = this.value <= that.value

}
  

… но я думаю, что должен быть лучший способ. Мысли?

Комментарии:

1. Ознакомьтесь с библиотекой Spire ( github.com/non/spire ). Однако это может быть излишним

2. @Eduardo Очень круто! Но я бы предпочел решение с использованием стандартной библиотеки.

3. как насчет неявного преобразования в базовый?

4. @Samar Определено где? В сопутствующем объекте? Будет ли это противоречить цели использования класса значений? Я думаю, что компилятор «преобразует» классы значений в базовое представление, чтобы избежать накладных расходов на фактическое создание экземпляров класса значений. Будет ли определение неявного преобразования вынуждать компилятор явно создавать экземпляры Price ?

5. Вы могли бы определить преобразование, в котором вы определяете класс значений. А затем импортируйте его туда, где вам это нужно. Я не думаю, что неявное преобразование в Double вызывает создание экземпляра класса значений в этом случае. Вы можете проверить вывод javap, чтобы убедиться в этом.

Ответ №1:

С помощью стандартной библиотеки Scala вы захотите реализовать Ordering , Numeric , или Fractional введите класс для Price . Это уточнения, где Ordering[A] <: Numeric [A] <: Fractional[A] . Для сравнений, например <= , вам просто нужно Ordering , для сложения, умножения, вычитания и т. Д. вам нужно Numeric и Fractional добавило бы разделение.

К сожалению, нет «быстрого доступа» к использованию Double функциональности, поэтому вам нужно, по крайней мере, написать средства пересылки для всех соответствующих методов:

 object PriceIsFractional extends Fractional[Price] {
  // Ordering:
  def compare(x: Price,y: Price): Int = x.value compare y.value

  // Numeric:      
  def plus (x: Price,y: Price): Price = Price(x.value   y.value)
  def minus(x: Price,y: Price): Price = Price(x.value - y.value)
  def times(x: Price,y: Price): Price = Price(x.value * y.value)
  def negate(x: Price): Price = Price(-x.value)
  def fromInt (x: Int): Price = Price(x.toDouble)
  def toInt   (x: Price): Int    = x.value.toInt
  def toLong  (x: Price): Long   = x.value.toLong
  def toFloat (x: Price): Float  = x.value.toFloat
  def toDouble(x: Price): Double = x.value

  // Fractional:
  def div(x: Price,y: Price): Price = Price(x.value / y.value)
}

// The following enables comparison operators:
import PriceIsFractional.mkOrderingOps

price1 <= price2   // works now

// The following enables numeric operators:
import PriceIsFractional.mkNumericOps

price1   price2    // works now
  

Комментарии:

1. Но здесь Price не распространяется на AnyVal?

2. Я не определяю Price , это точно так, как определено в вопросе OP. Я просто определяю класс типов для числовых операций.

3. @0__ можно ли определить эти операции для сопутствующего объекта для Price таким образом, чтобы они всегда были доступны без необходимости ручного импорта?

4. @davidrpugh Нет, в стандартных местах автоматически обнаруживаются только классы типов, методы расширения всегда должны иметь импорт. Однако вы можете просто поместить средства пересылки в сопутствующий объект. Нравится object Price { implicit def mkOrderingOps = ...; implicit def mkNumericOps = ... }

5. @0__ У меня возникли проблемы с получением неявных пересылок в сопутствующем объекте для работы. Можете ли вы дать мне более подробные сведения? Если это так, то я приму ваш ответ.

Ответ №2:

Следуя приведенному выше решению и с моей точки зрения, должно быть легко автоматически генерировать то, что вам нужно. В качестве альтернативы вы можете использовать вместо этого неявные макросы, которые вводятся и не требуют макросов paradise, но для аргументации это тривиальный способ пропустить шаблон.

Мы генерируем неявный экземпляр Functional[MyType] класса типов внутри сопутствующего объекта MyType , или в вашем случае мы генерируем сопутствующий объект object Price { implicit object bla extends Fractional[Price] { .. } }

Мы делаем это, потому что таким образом Scala может автоматически искать импликации внутри сопутствующего объекта, поэтому нам не требуется явный импорт.

     @macrocompat.bundle
    class FractionalMacro(val c: scala.reflect.macros.blackbox.Context) {

      import c.universe._

      /**
        * Retrieves the accessor fields on a case class and returns an iterable of tuples of the form Name -> Type.
        * For every single field in a case class, a reference to the string name and string type of the field are returned.
        *
        * Example:
        *
        * {{{
        *   case class Test(id: UUID, name: String, age: Int)
        *
        *   accessors(Test) = Iterable("id" -> "UUID", "name" -> "String", age: "Int")
        * }}}
        *
        * @param params The list of params retrieved from the case class.
        * @return An iterable of tuples where each tuple encodes the string name and string type of a field.
        */
      def accessors(
        params: Seq[c.universe.ValDef]
      ): Iterable[(c.universe.TermName, c.universe.TypeName)] = {
        params.map {
          case ValDef(_, name: TermName, tpt: Tree, _) => name -> TypeName(tpt.toString)
        }
      }

      def makeFunctional(
        tpe: c.TypeName,
        name: c.TermName,
        params: Seq[ValDef]
      ): Tree = {

        val fresh = c.freshName(name)
        val applies = accessors(params).headOption match {
          case Some(field) => field._1
          case None => c.abort(c.enclosingPosition, "Expected one arg")
        }

        q"""implicit object $fresh extends scala.math.Fractional[$tpe] {
// Ordering:
  def compare(x: $tpe, y: $tpe): Int = x.$field compare y.$field

  // Numeric:      
  def plus(x: $tpe,y: $tpe): $tpe = $name(x.$field   y.$field)
  def minus(x: $tpe,y: $tpe): $tpe = $name(x.$field - y.$field)
  def times(x: $tpe, y: $tpe): $tpe = $name(x.$field * y.$field)
  def negate(x: $tpe): $tpe = $name(-x.$field)
  def fromInt (x: Int): $tpe = $name(x.$field.toDouble)
  def toInt   (x: $tpe): Int    = x.$field.toInt
  def toLong  (x: $tpe): Long   = x.$field.toLong
  def toFloat (x: $tpe): Float  = x.$field.toFloat
  def toDouble(x: $tpe): Double = x.$field

  // Fractional:
  def div(x: $tpe, y: $tpe): $tpe = $name(x.value / y.value)
}
        }"""
      }
  def macroImpl(annottees: c.Expr[Any]*): Tree =
annottees.map(_.tree) match {
  case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$params) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
    :: Nil if mods.hasFlag(Flag.CASE) =>
    val name = tpname.toTermName

    val res = q"""
   $classDef
   object $name {
     ..${makeFunctional(tpname.toTypeName, name, params.head)}
   }
   """
    println(showCode(res))
    res

  case _ => c.abort(c.enclosingPosition, "Invalid annotation target, Sample must be a case classes")
  

}
}

Кроме того, вы можете ввести проверить поле, чтобы убедиться, что оно имеет известный математический тип, или использовать implicit Numeric , где это применимо, чтобы вы могли делегировать in scope implicit, если это необходимо.

Теперь практически все, что вам нужно, это:

 @compileTimeOnly("Enable macro paradise to expand macro annotations")
  class fractional extends StaticAnnotation {
    def macroTransform(annottees: Any*): Any = macro FractionalMacro.macroImpl
  }

@fractional case class Price(value: Double)
  

Возможны неявные макросы, если вы можете отредактировать содержимое, Fractional чтобы добавить ссылку на неявный макроматериализатор к его сопутствующему объекту, но поскольку в этом случае мы не можем редактировать библиотеку по умолчанию, это более простой способ не полагаться на явный импорт необходимых импликаций.

Бонус, это, вероятно, можно было бы расширить, чтобы иметь дело с большим количеством полей и более сложными вещами.