#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
чтобы добавить ссылку на неявный макроматериализатор к его сопутствующему объекту, но поскольку в этом случае мы не можем редактировать библиотеку по умолчанию, это более простой способ не полагаться на явный импорт необходимых импликаций.
Бонус, это, вероятно, можно было бы расширить, чтобы иметь дело с большим количеством полей и более сложными вещами.