LL脳がscalaの勉強を始めたよ その98


Scalaコップ本の28章の続きをやっていきますよ。28章はオブジェクトの等価性についてで、今回はパラメーター化された型の等価性の定義からやっていきますよ(`・ω・´)

パラメーター化された型の等価性の定義

前回までにやってきた等価性の定義では、パターンマッチ等を利用して被演算子型が適合するかどうかを判定してきたんのですが、パラメータ化されたクラスの場合は少し方法を変える必要があるみたいです。

とりあえず次のような二分木を表す抽象クラスTreeとその実装にequalsやhashCodeを追加するというサンプルとしてやっていきたいと思います。Tree関連のコードはこんなかんじですね

// 二分木を抽象クラスとして定義します
trait Tree[+T] {
  def elem:T
  def left:Tree[T]
  def right:Tree[T]
}

// 木が無い場合のオブジェクトです
object EmptyTree extends Tree[Nothing]{
  def elem = throw new NoSuchElementException("EmptyTree.elem")
  def left = throw new NoSuchElementException("EmptyTree.left")
  def right = throw new NoSuchElementException("EmptyTree.right")
}

// 部分木クラスを定義します
class Branch[+T](
  val elem:T,
  val left:Tree[T],
  val right:Tree[T]
) extends Tree[T]

コップ本的にはequalsやhashCodeの実装は抽象クラスの実装ごとに追加するべきデス!とのことなのでそれぞれについて考えていきますね。

Treeクラスへの追加

Treeクラスは抽象メソッドなのでなにもしません。こいつの実装ごとに考えていきます

EmptyTreeへの追加

EmptyTreeクラスではAnyRefから継承したequalsとhashCodeのデフォルト実装が適切に機能するので、こちらも放置でいいみたいです。

Branchへの追加

Branchが他のBranchと等価だと評価されるにはelem・left・rightの3つの値が全て等しい必要があるので、前回までと同様にequalsを追加するとすれば次のような書き方になりますな(´・ω・`)

// 部分木クラスにequalsメソッドを追加します
class Branch[+T](
  val elem:T,
  val left:Tree[T],
  val right:Tree[T]
) extends Tree[T] { 
  // すべての要素が等しいかどうかを判定します。
  override def equals (other:Any) = other match {
    case that:Branch[T] => this.elem == that.elem &&
                                        this.left == that.left &&
                                        this.right == that.right
    case _ => false
  }
}

しかしながらこいつをコンパイルすると怒られてしまいますね

warning: there were unchecked warnings; re-run with -unchecked for details
defined class Branch

とりあえず-uncheckedオプションを指定してどんなWarningなのかを見てみます

// Tのパターン型がチェックできません(#゚Д゚)というWarningみたいです
<console>:13: warning: non variable type-argument T in type pattern is unchecked since it is eliminated by erasure
           case that:Branch[T] => this.elem == that.elem &&
                     ^

15章でやったようにパラメータ化された型の要素型はコンパイラに消去されるので、実行時にチェックできない→パターンマッチ時に木構造の要素型であるTがチェック出来ていないというWarningみたいです。しかしながらコップ本によると、2個のBranchはフィールドが等しければ仮に要素型が異なっていても等しいと見なせるものなので、要素型のチェックはいらないとのことです。

また簡単に試してみることができるサンプルをあげてみますが、次のようにnilの値と2個のから部分木から構成されるBranch同士についても同じ物とみなしても良さそうです。

// WarningがでたBranchを無理やり動かして実際に比較してみますが
// 要素型はチェックされていないのでこの結果で問題なさげです
scala> val b1 = new Branch[List[String]](Nil, EmptyTree, EmptyTree)
b1: Branch[List[String]] = Branch@14f5a31

scala> val b2 = new Branch[List[Int]](Nil, EmptyTree, EmptyTree)
b2: Branch[List[Int]] = Branch@97aaa6

scala> b1 == b2
res0: Boolean = true

コップ本によると上記のような結果に対する解釈についてはクラスをどのように表現するかについてのメンタルモデルによって変わってくるとのことです。例えば次のような解釈ができるみたいですが、Scalaでは前者の型消去モデルを採用しているのでb1, b2が等しいと見るらしいです(´・ω・`)

  • 型パラメータがコンパイル時にしか存在しないモデル
    • 上記b1, b2の2つのBranchが等しいと見るのが自然
  • 型パラメータをオブジェクトの値の一部とするモデル
    • 上記b1, b2の2つのBranchは異なると見るのが自然

問題ないという言いつつもunchecked warningは正直うっとおしいのです(´・ω・`)これを止めるにはパターンマッチ部分で、要素型としてTのかわりにtを使えばいいのだとか

class Branch[+T](
  val elem:T,
  val left:Tree[T],
  val right:Tree[T]
) extends Tree[T] { 
  override def equals (other:Any) = other match {
    // 要素型をtに変えます
    case that:Branch[t] => this.elem == that.elem &&
                                        this.left == that.left &&
                                        this.right == that.right
    case _ => false
  }
}

上記のように要素型を小文字で表現すると未知の要素型を表すようになるのでuncheked warningを回避できるそうです。なお、次のようにアンダースコアを使ってもよいとのことです。

case that:Branch[_]

equalsの追加ができたところでhasCodeとcanEqualを追加してみますよ

class Branch[+T](
  val elem:T,
  val left:Tree[T],
  val right:Tree[T]
) extends Tree[T] { 
  override def equals (other:Any) = other match {
    case that:Branch[t] => this.elem == that.elem &&
                                        this.left == that.left &&
                                        this.right == that.right
    case _ => false
  }
  // とりあえず適当にばらけさせればいいので…
  // というhashCode実装の一例
  override def hashCode:int =
    41 * (
      41 * (
        41 + elem.hashCode
      ) + left.hashCode
    ) + right.hashCode
  // 型付きのパターンマッチで表現してみます
  def canEqual(other:Any) = other mtach {
    case that:Branch[_] => true
    case _ => false
  }
}

ちなみに上記canEqualはisInstanceOfを使って次のようにも実装できるみたいですね(´・ω・`)

def canEqual(other:Any) = other.isInstanceOf[Branch[_]]
Branch[_]の意味

先ほど出てきたBranch[_]というアンダースコアを利用した表現は型パターンではなくてメソッドの型パラメータらしいです。これは存在型という未知の部分を含んだ型というもので、詳細内容については次の章で取り扱うみたいです(´・ω・`)

厳密にはパターンマッチ内とメソッド呼び出しの型パラメータでは異なるものを表現するのですが、本質的には同じ意味で、アンダースコアを使うことで未知のモノにラベルを付けることができるYO!とのことです。

いじょうー

とりあえず今回はこんなところで、若干写経気味だったのであとでやり直さないとな…と。次回はequalsとhashCodeのレシピからやりますデス。次回で28章終わりたいなぁ…が、頑張ります(´・ω・`)