🔀

REPLで体感する Scala 2のOrdered入門 ― 自作クラスを「<」とsortedで並べ替える

に公開

— 「< や sorted が自作クラスでも使えるって本当?」を確かめる

0. 前提:なぜ Ordered が要るの?

  • List などの並べ替えには「順序づけ」が必要
  • 既存型(Int, String)は最初から順序が定義されている
  • 自分のクラスでも < <= > >= や sorted を使いたい → そのための仕組みが Ordered[T]

Ordered[T] を ミックスインして compare(that: T): Int を 1 つ実装すれば、
比較演算子一式と min/max/sorted などが自動で使えるようになります。

1. REPL 準備

scala

以降は コピペして Enter で進められます。出力は例です(res0 等の番号は環境で変わります)。

2. Rational(有理数)を Ordered にする

2-1. まずは素のクラス

scala> class Rational(n: Int, d: Int) {
     |   require(d != 0, "denominator must be non-zero")
     |   private val g = BigInt(n).gcd(BigInt(d)).toInt
     |   val numer: Int = n / g
     |   val denom: Int = d / g
     |   override def toString = s"$numer/$denom"
     | }
class Rational

2-2. まだ比較できない確認

scala> val half = new Rational(1, 2)
val half: Rational = 1/2

scala> val third = new Rational(1, 3)
val third: Rational = 1/3

scala> half < third
            ^
       error: value < is not a member of Rational

2-3. Ordered[Rational] をミックスイン

scala> class OrdRational(n: Int, d: Int) extends Rational(n , d) with Ordered[OrdRational] {
     |   // a/b ? c/d を a*d と c*b で比較 (クロス乗算)
     |   def compare(that: OrdRational): Int =
     |     (this.numer * that.denom) - (that.numer * this.denom)
     | }
class OrdRational

2-4. 比較演算子が全部使える!

scala> val a = new OrdRational(1, 2) // 1/2
val a: OrdRational = 1/2

scala> val b = new OrdRational(1, 3) // 1/3
val b: OrdRational = 1/3

scala> a < b
val res1: Boolean = false

scala> a > b
val res2: Boolean = true

scala> a <= b
val res3: Boolean = false

scala> a >= b
val res4: Boolean = true

scala> List(a, b, new OrdRational(3, 4)).sorted
val res5: List[OrdRational] = List(1/3, 1/2, 3/4)

ポイント

  • 実装したのは compare だけ。演算子は Ordered が面倒を見てくれます。
  • sorted が動く=全順序(total order)が提供できている、ということ。

3. equals / hashCode も整える(Set/Mapで効く)

Ordered は 順序を与えるだけで 等値は関知しません。
Set/Map のキーにするなら、equals と hashCode は約束どおりに。

scala> class KeyRational(n: Int, d: Int) extends OrdRational(n, d) {
     |   override def equals(obj: Any): Boolean = obj match {
     |     case that: KeyRational => 
     |       this.numer == that.numer && this.denom == that.denom
     |     case _ => false
     |   }
     |   override def hashCode(): Int = 41 * (41 + numer) + denom
     | }
class KeyRational

確認:

scala> val s = scala.collection.mutable.Set[KeyRational]()
val s: scala.collection.mutable.Set[KeyRational] = HashSet()

scala> s += new KeyRational(1, 2); s += new KeyRational(2, 4); s
val res6: scala.collection.mutable.Set[KeyRational] = HashSet(1/2)

1/2 と 2/4 は既約形が同じ → 1 要素にまとまる(equals/hashCode 整備の効果)。

4. もう一つ:自然順ではない独自順序(長さ順など)

Ordered をミックスインする代わりに、別に Ordering[T] を用意して sorted に渡す方法もあります(比較だけ差し替えたい時に便利)。

scala> val byLen: Ordering[Vec2] =
     |   Ordering.by(v => v.x*v.x + v.y*v.y) // 長さの2乗で比較
val byLen: Ordering[Vec2] = scala.math.Ordering$$anon$5@6ff08304

scala> val vs = List(Vec2(3, 4), Vec2(1, 1), Vec2(0, 2))
val vs: List[Vec2] = List(Vec2(3.0,4.0), Vec2(1.0,1.0), Vec2(0.0,2.0))

scala> vs.sorted(byLen)
val res7: List[Vec2] = List(Vec2(1.0,1.0), Vec2(0.0,2.0), Vec2(3.0,4.0))

5. まとめ(今日使える実用レシピ)

  1. 自作クラスを並べ替えたい → class X extends Ordered[X] { def compare(that:X):Int = ... }
  2. Set/Map で重複排除もさせたい → equals / hashCode を 比較基準と一致させる
  3. 用途ごとに順序を切替 → Ordering[T] を用意して sorted(ordering) を使う

Discussion