在Scala中创建支持比较和数学运算的值类的什么是自动方式?假设我有以下值类...
case class Price(value: Double) extends AnyVal
我希望能够做到......
val price1 = Price(23.4)
val price2 = Price(1.0)
price1 <= price2
price1 + price2
...等所有其他比较和数学运算符。一种解决方案是手动实现每种所需方法......
case class Price(value: Double) extends AnyVal {
def <=(that: Price): Boolean = this.value <= that.value
}
......但我认为必须有更好的方法。想法?
答案 0 :(得分:3)
使用标准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 :(得分:0)
按照上面的解决方案,从我的头顶开始,应该很容易自动生成你需要的东西。你可以选择使用隐式宏,它们是键入的,不需要宏天堂,但为了论证,这是跳过样板的一种简单方法。
我们正在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")
} }
此外,您可以键入检查字段以确保其具有已知数学类型,或者在适用的情况下使用隐式Numeric
,以便在需要时可以隐式委托范围。
现在几乎所有你需要的是:
@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
的内容以将隐式宏构造函数的引用添加到其伴随对象,则隐式宏是可能的,但是因为在这种情况下我们无法编辑默认库,所以这是一个更冷静的方式,不必依赖明确导入必要的隐含。
奖金,这可能会扩展到处理更多领域和更复杂的事情。