Scala

独書会 Scala IN DEPTH @夜のイタリアンカフェ その5

More than 3 years have passed since last update.

独書会 Scala IN DEPTH @大人の喫茶店 その4で記述した、第2章 The core rules の続き。

要約

Polymorphic equality

equals hashCode関数の実装は、polymorphic言語では注意が必要だが、次のルールに従がえば扱いやすくなる。

一般的に、特に参照のequalityのようなequalityを強く必要とするクラスでは、複数の具体的なレベルを持つことを避けるのがベストである。

  • あるケースでは、クラスは参照のequalityのみを必要とする。参照のequalityとは、同じインスタンスであるかどうかを判断するために、2つのオブジェクトを区別する。
  • equalityの比較は、2つの異なるインスタンスが等価であることや、複数の具体的な階層であることを判断する必要がある。

Example: A timeline library

タイムライン、カレンダー、ウィジェットを作る。

ウィジェットに必要なもの。

  • dates
  • times
  • time ranges
  • それぞれの日に関連付けられたイベント

InstantaneousTime

このライブラリの基本的な概念。
InstantaneousTimeは、時系列内の特定の離散時間を表すクラス。
Gregorian calendarで値を持つ。

基本的な時間を、グレゴリオ暦のグリニッジ標準時の1970年1月1日午前0時からの秒の整数で持つようにする。
他の全ての時間をこの表現にフォーマットできると仮定する。タイムゾーンは表現に対して直交する関心事である。

equality使用についての一般的な仮定

  • equalsが呼ばれた場合、trueを返す。この場合、2つのオブジェクトは同じ参照を持つ。
  • equalsの呼び出しのほとんどが、falseを返すことになる。
  • hashCodeの実装は、hashCodesが異なるので、equality比較のためには十分希薄である。
  • hashCode計算は、深いequality比較よりも効率的である。
  • 参照のequalityをテストすることは、深いequality比較よりも効率的である。

これらの仮定は、ほとんどのequalityの実装で標準である。

Listing 2.22 Simple InstantaneousTime class
trait InstantaneousTime {
val repr: Int
  override def equals(other: Any) : Boolean = other match {
    case that: InstantaneousTime =>
       if(this eq that) {
         true
       } else {
         (that.## == this.##) &&
         (repr == that.repr)
       }
    case _ => false
}
  override def hashCode() : Int = repr.##
}

(Joshua D. Suereth, Scala in Depth, p.39)

  • このクラスのメンバーは、グリニッジ標準時の1970年1月1日午前0時からの秒を表す数値を持つ reprのみである。
  • reprはこのクラスで唯一のデータであり、しかもimmutableであり、equalshasCodeはこの値に基づいて行われる。
  • JVM内でequalsメソッドを実装する時は、深いequalityチェックの並びにする前に、参照のequalityでテストすることは、効率が良くなる。
  • 特に複雑なクラスでは劇的にパフォーマンスが向上するが、このクラスでは対して必要としない。
  • 早期のfalseチェックのために、hashCodeを使う。hashCodeを計算することはわずかで簡単なので、良いアイデアである。

## AND == VS. EQUALS AND HASHCODE

  • ==メソッドは、Javaのequalsメソッドに相当
  • ##メソッドは、JavaのhashCodeメソッドに相当

equalshashCodeメソッドを呼び出す時は、## ==を使う方が良い。これらのメソッドは値型のサポートをしている。

2つの原理

  • 良いequalityメソッドの重要性
  • コードの仮定に常に挑戦すること

このケースでのベストプラクティスequalityメソッドは、シンプルなクラスに利益を与える。
独自のクラスのためにequalityを実装する時、trueを持っていることを確認するために、標準的なequalityの実装で仮定をテストする。

equalsの実装は、polymorphismの欠陥に苦しむ。

Polymorphic equalsの実装

一般的に、深いequalityを必要等する型で多態性を避けるのがベスト。
Scalaではこれを理由に、ケースクラスのサブクラス化をサポートしていない。

Listing 2.23 Event subclass of InstantaneousTime
trait Event extends InstantaneousTime {
  val name: String
  override def equals(other: Any): Boolean = other match {
    case that: Event =>
      if(this eq that) {
        true
      } else {
        (repr == that.repr) &&
        (name == that.name)
      }
    case _ => false
  }
}

(Joshua D. Suereth, Scala in Depth, p.40)

タイムライン上のイベントを保持するInstantaneousTimeのサブクラスEventクラスを作成した。
2つのEventオブジェクトだけが等しくなるように、パターンマッチを変更している。

Listing 2.24 Using Event and InstantaneousTime
scala> val x = new InstantaneousTime {
     | val repr = 2
     |}
x: java.lang.Object with InstantaneousTime = $anon$1@2
scala> val y = new Event {
     | val name = "TestEvent" | val repr = 2
     |}
y: java.lang.Object with Event = $anon$1@2
scala> y == x
res8: Boolean = false
scala> x == y
res9: Boolean = true

(Joshua D. Suereth, Scala in Depth, p.40)

上記のREPLを試してみる。
古いクラスは、equalityメソッドの古い実装を使っているので、新しい名前のフィールドをチェックしない。
サブクラスではequalityの意味を変更するかもしれない事実を考慮し、基本クラスで元のequalityを変更する必要がある。

Scalaではこの問題に対応するscala.Equalsトレイトがある。
Equalsトレイトは、標準のequalsメソッドと連携して使われるcanEqualメソッドを定義する。

canEqualメソッドはサブクラスに親クラスのequality実装を見合わせることを許す。
これは、equalsメソッド内の他のパラメータがequalityの失敗で引き起こす機会を許すことにより行われる。

equalsメソッドをオーバライドした拒否基準が持つ何かでサブクラス内のcanEqualをオーバライドする。

Listing 2.25 Using scala.Equals
trait InstantaneousTime extends Equals {
  val repr: Int
  override def canEqual(other: Any) =
    other.isInstanceOf[InstantaneousTime]
  override def equals(other: Any) : Boolean =
    other match {
      case that: InstantaneousTime =>
        if(this eq that) true else {
             (that.## == this.##) &&
             (that canEqual this) &&
             (repr == that.repr)
        }
    case _ => false
}
  override def hashCode(): Int = repr.hashCode
}
trait Event extends InstantaneousTime {
  val name: String
  override def canEqual(other: Any) =
    other.isInstanceOf[Event]
  override def equals(other: Any): Boolean = other match {
    case that: Event =>
      if(this eq that) {
        true
      } else {
        (that canEqual this) &&
        (repr == that.repr) &&
        (name == that.name)
      }
    case _ => false
  } 
}

(Joshua D. Suereth, Scala in Depth, p.40)

  • 他のオブジェクトがInstantaneousTimeである場合にtrueを返すようにInstantaneousTimecanEqualを実装する。
  • equalityの実装内で、オブジェクト内のcanEqualを戻す。
  • Eventクラス内でオーバライドされたcanEqualは他のEventsでだけequalityが許される。

親クラスのequalityをオーバライドする時は、canEqualもオーバライドする

  • canEqualメソッドは、親クラスのequality実装を抜け出すことをサブクラスに許す手段である。
  • サブクラスは、trueを返す親クラスのequalsメソッドに関連付けられた通常の危険性をなくして行うことを許す。
  • サブクラスは、同じ2つのオブジェクトに対してfalseを返す。
Listing 2.26 Using new equals and canEquals methods
scala> val x = new InstantaneousTime {
     | val repr = 2
     |}
x: java.lang.Object with InstantaneousTime = $anon$1@2
scala> val y = new Event {
     | val name = "TestEvent" | val repr = 2
     |}
y: java.lang.Object with Event = $anon$1@2
scala> y == x
res10: Boolean = false
scala> x == y
res11: Boolean = false

(Joshua D. Suereth, Scala in Depth, p.40)

最終的にpolymorphicでも対応したモノで、REPLしてみた結果。

Summary

  • Scalaを使うための最初の重要な項目を見た。
  • REPL を活用することにが、Scala開発者として成功するには重要。
  • 式指向プログラミングとimmutableを好むことにより、プログラムを簡素化し、コードを推論する能力を向上させることができる。
  • Optionにより、初期化されていない値を受け入れられる場所を明確にし、コードの妥当性を高めることができる。
  • 多態の下で、適切なequalityメソッドを記述することは困難。

上記のプラクティスのすべてが、Scalaの成功のための最初のステップとなる。

Rule

5. 多態のequalityのためにscala.Equalsを使う

多態のequalityは、簡単におかしくなる。
scala.Equalsには、間違いを避けるための簡単なテンプレートが提供されている。

所感

  • Polymorphic 、多態・多相というよりは多型で良いのかな?
  • ドメインオブジェクトにはやっぱりequalshashCodeを実装しよう。
  • scala.Equalsも使っていこう。