高卒文系Scala vol.16 - 業務で使えるRationalとは? -

Scala本、6章のRational(分数)を写経してみました。

21章の暗黙の型変換と、28章のequalsメソッドとかもついでに実装。

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミングコンセプト&コーディング

現段階で、業務で使えそうな実装をちょこっとだけ取り入れたつもりです。

変数名は自分好みに、コメントはScalaAPIのソースコードのコメントを参考にするなど、地味にいじってます。

「//」のコメントの所は、自分のメモ用。

Rational

/*
* Rational.scala
*/
package sample.vo

/**
* 分数を表すRationalのコンパニオンオブジェクト
* @author zetta1985
*/
object Rational{
  /**指定された分子・分母で新しいRationalを生成して返す。
  * @param n 分子
  * @param d 分母
  */
  def apply(n: Int, d: Int): Rational = new Rational(n, d)

  /**分子が指定されたInt、分母が1の新しいRationalを生成して返す。
  * @param n 分子
  */
  def apply(n: Int): Rational = new Rational(n, 1)

  /**指定されたBigDecimalで新しいRationalを生成して返す。
  * @param d 任意精度の符号付き10進数
  */
  def apply(d: BigDecimal): Rational =
    new Rational(d.bigDecimal.movePointRight(d.scale).intValue,
      Math.pow(10, d.scale).toInt)

  // 以下、暗黙の型変換メソッド
  /**指定されたIntを、Rationalに変換して返す。
  */
  implicit def int2Rational(n: Int): Rational = Rational(n)

  /**指定されたDoubleを、Rationalに変換して返す。
  */
  implicit def bigDeciaml2Rational(dec: BigDecimal): Rational = Rational(dec)
}

/**
* 分数を表すRationalのコンパニオンクラス
* @author zetta1985
*/
class Rational private(n: Int, d: Int) {
  require(d != 0) // 分母が0の場合はエラー

  import sample.util.MathUtil._

  /** 最大公約数 */
  private val gratestCommonDiviser = getGratestCommonDiviser(n.abs, d.abs);

  /** 分子 */
  val numerator: Int = (if (d < 0) -n else n) / gratestCommonDiviser

  /** 分母 */
  val denominator: Int  = d / gratestCommonDiviser

  /** 指定されたRationalをこのRationalに加算した値を返す。 */
  def + (that: Rational): Rational =
    new Rational(
      numerator * that.denominator + that.numerator * denominator,
      denominator * that.denominator
    )

  /** 指定されたIntをこのRationalに加算した値を返す。 */
  def + (num: Int): Rational = this + Rational(num)

  /** 指定されたBigDecimalをこのRationalに加算した値を返す。 */
  def + (num: BigDecimal): Rational = this + Rational(num)

  /** 指定されたRationalをこのRationalに減算した値を返す。 */
  def - (that: Rational): Rational =
    new Rational(
      numerator * that.denominator - that.numerator * denominator,
      denominator * that.denominator
    )

  /** 指定されたIntをこのRationalに減算した値を返す。 */
  def - (num: Int): Rational = this - Rational(num)

  /** 指定されたBigDecimalをこのRationalに減算した値を返す。 */
  def - (num: BigDecimal): Rational = this - Rational(num)

  /** 指定されたRationalをこのRationalに乗算した値を返す。 */
  def * (that: Rational): Rational =
    new Rational(numerator * that.numerator, denominator * that.denominator)
  /** 指定されたIntをこのRationalに乗算した値を返す。 */
  def * (num: Int): Rational = this * Rational(num)

  /** 指定されたBigDecimalをこのRationalに乗算した値を返す。 */
  def * (num: BigDecimal): Rational = this * Rational(num)

  /** 指定されたRationalをこのRationalに除算した値を返す。 */
  def / (that: Rational): Rational =
    new Rational(numerator * that.denominator, denominator * that.numerator)

  /** 指定されたIntをこのRationalに除算した値を返す。 */
  def / (num: Int): Rational = this / Rational(num)

  /** 指定されたBigDecimalをこのRationalに除算した値を返す。 */
  def / (num: BigDecimal): Rational = this / Rational(num)

  /** 指定されたAnyとこのRationalが等値である場合、trueを返す。 */
  override def equals(other: Any): Boolean =
    other match {
      case that: Rational =>
        (that canEqual this) &&
        numerator == that.numerator &&
        denominator == that.denominator
      case _ => false
    }

  /**指定されたAnyがこのオブジェクトのクラスである場合、trueを返す。
  */
  // サブクラスequalsオーバーライド用
  def canEqual(other: Any): Boolean = other.isInstanceOf[Rational]

  /** このRationalのハッシュコードを返す。 */
  override def hashCode: Int = 41 * (41 + numerator) + denominator

  /** このRationalの文字列表現を返す。 */
  override def toString = numerator + "/" + denominator

  import scala.BigDecimal.RoundingMode._

  /** このRationalをBigDecinalに変換する。*/
  def bigDecimalValue(scale: Int, rMode: RoundingMode) : BigDecimal =
    (BigDecimal(numerator).setScale(scale, rMode) /
    BigDecimal(denominator).setScale(scale, rMode)).setScale(scale, rMode)
}

業務上でよく使われるであろう、scala.BigDecimalに対応してみました。

scala.BigDecimalはjava.math.BigDecimalをラップしていて、 bigDecimal:java.math.BigDecimalというpublicフィールドが定義されています。

Scalaの標準APIで業務でよく使いそうなクラスの使い方メモ、なんてのも後々書いていきたいですねー

比較系のメソッドが無いので、まだ実務には程遠い実装ですね・・・

あと、だいぶオブジェクトが無駄に生成されてます。 Intをパラメータにとる+メソッドなどは、Scala本では、ちゃんと分子分母で計算して出してますが、 可読性重視で今のような実装に。

もし、パフォーマンスに問題が生じた場合は、FastRationalっとかいうサブクラス作って、 Scala本通りの実装にすればいいかなーとは思います。 サブクラス対応してますし。

最大公約数を取得するメソッドは、他のクラスでも使いたいでしょうし、 Rationalに定義するのは責務が分離できてないかなーと思い、 MathUtilオブジェクトというユーティリティオブジェクトで定義するようにしました。

MathUtil

/*
* MathUtil.scala
*/
package sample.util

/**
* 数値操作のユーティリティオブジェクト
* @author zetta1985
*/
object MathUtil {
  /** aとbの最大公約数を返す。*/
  def getGratestCommonDiviser(a: Int, b: Int): Int =
    if (b == 0) a else getGratestCommonDiviser(b, a % b)
}

今のところはこんだけw

Main(エントリーポイント)

/*
* Main.scala
*/
package sample
import sample.vo._
import sample.vo.Rational._ // コンパニオンオブジェクトのimport

object Main extends Application{
  import scala.BigDecimal.RoundingMode._
  val r = Rational(5, 10)
  println(r)
  println(r.bigDecimalValue(5, ROUND_HALF_DOWN))
  println(Rational(BigDecimal(1.23).setScale(2, ROUND_HALF_UP)))
  println
  println(r + 1.2)
  println(r + BigDecimal(1.2))
  println(r + BigDecimal(1.2).setScale(1, ROUND_HALF_DOWN))
  println(r - BigDecimal(0.45).setScale(2, ROUND_HALF_DOWN))
  println(r * 1.2)
  println(r / 4)
  println
  println(2 + r)
  println(BigDecimal(1.5) + r)
  println
  println(r == Rational(50, 100))
  println(r == Rational(6, 10))
}

実行結果

1/2
0.50000
123/100
-2072361605/-2
-2072361605/-2
17/10
1/20
-2109922627/-2
1/8
5/2
2/1
true
false

うん、なんだかキモイ事になってますねぇ・・・

問題はBigDecimal(Double)の所。

1.2が1.1999999999999999555910790149937383830547332763671875になってしまうので、

分子と分母の正規化が意図通りにいかない。

BigDecinalでスケールと丸めモードを指定してあげると、意図通りの結果が得られるのですが・・・

実際の業務でRationalを使う場合の課題

もちろん、前述のRationalのコードをそのまま実際の業務で使う事はできないので、

実装を見直す必要があります。

すぐに思いついたモノだけ挙げてみます。

Rationalに比較用のメソッドを定義する

<、>、<=、>=とか。comparaToをまず実装して、その結果で判定するのが楽。

ファクトリーメソッド(apply)のパラメータ型

Stringはサポート方が、他のレイヤとインターフェイスしやすいかも?

Float・Double辺りは用途が無さそうだけど、現状だとFlout・Doubleをapplyに指定した場合、

暗黙の型変換されてスケールが設定されてないBigDecimalでRationalを生成されてしまう・・・

BigIntegerを考慮するなら、用意する必要有り。クライアントの暗黙の型変換を頼りにしてはいけない。

BigDecimalはスケール設定されてない値をどうするかが悩みどころ。

演算子メソッドのパラメータ型

一つ上と一緒。Float・Doubleの扱いをどうするか。

分子・分母フィールドのカプセル化

不変オブジェクトとして徹底するなら、アクセス可能性をサブクラスまで限定する。

その場合、クライアント側で分子・分母を知りたいケースを考慮し、別途ゲッターを定義する。

文字列表現のフォーマット

フォーマット文字列で書式を設定できるようにすると、アプリケーションレイヤとかで扱いやすくなる。

これもどこまでサポートするか、というのが問題になりそうなので、

基本的なモノだけ実装しといて、あとはサブクラスで~というのが現実的か?

こんなところでしょうか。まだまだ課題は多いですね・・・

vol.100まで続いたら、取り組みますw

このエントリーをはてなブックマークに追加
comments powered by Disqus