2016-10-10 10 views
2

スカラでサポート比較と数学演算をサポートする値クラスを作成する方法は何ですか?等...私は...比較演算演算をサポートするスカラ値クラス

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

price1 <= price2 
price1 + price2 

のようなものを行うことができるようにしたいと思い

case class Price(value: Double) extends AnyVal 

...私は次の値のクラスを持っている他のすべての比較と算術演算子のために仮定します。 1つの解決策は、それぞれの望ましい方法を手作業で実装することです...

case class Price(value: Double) extends AnyVal { 

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

} 

...しかし、私はよりよい方法が必要であると思います。思考?

+0

Spireライブラリ(https://github.com/non/spire)をご覧ください。しかし、余計かもしれませんが – Eduardo

+0

@Eduardoとてもクールです!しかし、私は標準ライブラリを使用したソリューションを好むでしょう。 – davidrpugh

+0

潜在的な暗黙的な変換はどうですか? – Samar

答えて

3

標準のScalaライブラリでは、PriceOrderingNumeric、またはFractionalタイプのクラスを実装したいと考えています。これらは洗練されたもので、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 
+0

しかし、ここで価格はAnyValを拡張しないのですか? –

+0

私は 'Price'を定義しません。これはOPの質問に定義されたものとまったく同じです。私は単純に数値演算の型クラスを定義します。 –

+0

@ 0__ Priceのコンパニオンオブジェクトでこれらの操作を定義することは、手動でインポートすることなくいつでも利用できるような方法ですか? – davidrpugh

0

私の頭の上、上記とオフを解決した後、必要なものを自動生成するのは簡単なはずです。代わりに暗黙のマクロを使用することもできます。これは型付けされ、マクロ・パラダイスは必要ありませんが、議論のために、これは定型文をスキップする簡単な方法です。私たちは、MyTypeのコンパニオンオブジェクトの内部Functional[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) 

暗黙のマクロがで以来、あなたはそのコンパニオンオブジェクトへの暗黙的なマクロmaterialiserへの参照を追加するためにFractionalの内容を編集することができれば、可能性がありますが、この場合、デフォルトのライブラリを編集することはできません。これは、必要な暗黙のインポートを明示的にインポートする必要がない、よりクールな方法です。

ボーナスは、おそらくより多くのフィールドとより複雑なものに対応するために拡張される可能性があります。