Scala值类,支持比较和数学运算

时间:2016-10-10 04:55:00

标签: scala

在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

}

......但我认为必须有更好的方法。想法?

2 个答案:

答案 0 :(得分:3)

使用标准Scala库,您需要为Ordering实现NumericFractionalPrice类型类。这些是改进,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的内容以将隐式宏构造函数的引用添加到其伴随对象,则隐式宏是可能的,但是因为在这种情况下我们无法编辑默认库,所以这是一个更冷静的方式,不必依赖明确导入必要的隐含。

奖金,这可能会扩展到处理更多领域和更复杂的事情。