独書会 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であり、`equals`と`hasCode`はこの値に基づいて行われる。
- JVM内で`equals`メソッドを実装する時は、深いequalityチェックの並びにする前に、参照のequalityでテストすることは、効率が良くなる。
- 特に複雑なクラスでは劇的にパフォーマンスが向上するが、このクラスでは対して必要としない。
- 早期の`false`チェックのために、`hashCode`を使う。`hashCode`を計算することはわずかで簡単なので、良いアイデアである。
### `##` AND `==` VS. EQUALS AND HASHCODE
- `==`メソッドは、Javaの`equals`メソッドに相当
- `##`メソッドは、Javaの`hashCode`メソッドに相当
`equals`と`hashCode`メソッドを呼び出す時は、`##` `==`を使う方が良い。これらのメソッドは値型のサポートをしている。
### 2つの原理
- 良い`equality`メソッドの重要性
- コードの仮定に常に挑戦すること
このケースでのベストプラクティス`equality`メソッドは、シンプルなクラスに利益を与える。
独自のクラスのために`equality`を実装する時、`true`を持っていることを確認するために、標準的なequalityの実装で仮定をテストする。
`equals`の実装は、polymorphismの欠陥に苦しむ。
### Polymorphic equalsの実装
一般的に、深いequalityを必要等する型で多態性を避けるのがベスト。
Scalaではこれを理由に、ケースクラスのサブクラス化をサポートしていない。
> ```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`をオーバライドする。
> ```scala: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
を返すようにInstantaneousTime
でcanEqual
を実装する。 - 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__ 、多態・多相というよりは多型で良いのかな?
- ドメインオブジェクトにはやっぱり`equals`と`hashCode`を実装しよう。
- `scala.Equals`も使っていこう。