Scala学習メモ
等価性
ScalaとJavaの違い
両言語の取扱いの違いについて下記にまとめる。
Scala | Java | |
---|---|---|
値の比較 | == |
== |
オブジェクトの比較(値等価) | == |
equals |
オブジェクトの比較(参照等価) | eq |
== |
オブジェクトの比較(ユーザ定義等価) |
== (override equals ) |
override equals |
等価性実装のポイント
class A(val value: String) {
var mutatedValue: String /* 3. */
override def equals(x: Any /* 1. */) = x match {
case xx: A => value == xx.value
case _ => false
}
override def hashCode = value.## /* 2. */
}
-
Any#equals
をオーバーライドする- 引数の型はクラスの型ではなく
Any
になる
- 引数の型はクラスの型ではなく
-
hashCode
は常にequals
で使っている値をすべて使って常に同じ計算結果となるようにする-
##
で簡単にハッシュ値を作れる
-
- ミュータブルなプロパティを
equals
に使用してはいけない - 下記条件を満たすようにする
x.equals(x) == true // x is not null
x.equals(y) == y.equals(x) // x, y is not null
when x.equals(y) == y.equals(z), x.equals(z) == true // x, y, z is not null
x.equals(null) == false // x is not null
- 冪等である
継承関係にあるクラスでのポイント
継承関係にあるクラスでの等価性は、基本的に 厳格 にしたほうが良い場合が多い。
例えば、下記のような Text
と AttributedText
のクラスでは、比較結果が true
になるパターンをあれこれ考えるより、相互に比較結果が false
になるように実装したほうが比較時の違和感が少ない。
class Text(val value: String) {
override def equals(x: Any) = x match {
case xx: Text => canEqual(x) && value == xx.value
case _ => false
}
override def hashCode = value.##
def canEqual(x: Any) = x.isInstanceOf[Text]
// 一部の等価性を比較したい場合は`==`ではなく独自にメソッドを実装したほうがわかりやすい
def equalsText(x: Text) = value == x.value
}
class AttributedText(override val value: String, val attributes: Map[String, Any]) extends Text(value) {
override def equals(x: Any) = x match {
case xx: AttributedText => canEqual(x) && super.equals(x) && attributes == xx.attributes
case _ => false
}
override def hashCode = (value, attributes).##
override def canEqual(x: Any) = x.isInstanceOf[AttributedText]
}
上記のように canEqual
を実装し、インスタンス判定を条件に加えることで実現できる。
また、こうしておくと、無名サブクラスでも正しく判定されるようになる。
val text = new Text("test")
val anotherText = new Text("sample") { override val value: String = "test" }
println(text == anotherText)
// ==> true
変位パラメータ
前提
前提として、計数可能な性質を表す Countable
を定義する。
Countable
は説明と数量を保持する。
trait Countable {
val description: String
def number: Int
}
ここでは、上記の性質をテーブルゲームに適用し、具体化したものとして、将棋の駒数、麻雀牌の数をモデリングしてみる。
abstract class TableGame extends Countable {
val players: Int
}
case object Shogi extends TableGame {
override val description = "将棋の駒数"
override val number = 40
override val players = 2
}
case object Mahjong extends TableGame {
override val description = "麻雀牌の数"
override val number = 136
override val players = 4
}
上限境界で与えられる型を限定する
Countable
の数量が最大であることを表す型 Max
を定義してみる。
Max
は型パラメータ T
を持っており、最大値を持つオブジェクトを value
として保持する。
また、与えられた値 T
を自身と比較し、より大きい値を持つ Max
を返却するメソッド max
も実装する。
case class Max[T](initial: T) {
val value = initial
def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
}
ただし、このままだと T
が Countable
を継承しない型をとれるため、 max
でコンパイルエラーとなってしまう。
value number is not a member of type parameter T
そこで、 T
が Countable
の性質をもつことを示すため、 上限境界 を設定する。
- case class Max[T](initial: T) {
+ case class Max[T <: Countable](initial: T) { // <: が上限境界の指定
val value = initial
def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
}
こうすることで、 T
は Countable
のサブクラスのインスタンスまたはミックスインされたオブジェクトに限定することができるため、 number
が参照できるようになる。
次のコードを実行すると結果が表示される。
val max = Max(Shogi).max(Shogi)
println(s"${max.value.description}は${max.value.number}です")
// ==> "将棋の駒数は40です。"
max
をより有用な処理にするために
先ほどの例では、同じものを比較しており、意味を感じられるものではなかった。
では次のコードを実行するとどうなるだろうか。
val max = Max(Shogi).max(Mahjong)
println(s"${max.value.description}は${max.value.number}です")
少しは意味がありそうなコードになったが、実際に実行しようとするとコンパイルエラーとなる。
type mismatch;
found : Mahjong.type
required: Shogi.type
Max(Shogi)
のインスタンス型は Max[Shogi]
なので、 max
が受け取る引数の型も、返却する Max
の型パラメータも Shogi
を要求しているのである。これに対応するためには Countable
を取り扱うようにすれば良い。
case class Max[T <: Countable](initial: T) {
val value = initial
- def max(x: T): Max[T] = if (x.number > value.number) Max(x) else this
+ def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
}
しかし、依然としてコンパイルエラーとなる。
type mismatch;
found : Max[T]
required: Max[Countable]
Note: T <: Countable, but class Max is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
これは、 max
の定義として、 Max[Countable]
を返却する宣言なのに Max[T]
を返却しているためである。
非変(invariant)と共変(co-variant)
このエラーに対処するためには非変と共変について理解しておく必要がある。
B extends A
であるとき、 C[T]
について C[B] extends C[A]
を満たすときの T
を 共変 という。
一方 C[B]
と C[A]
をそれぞれ独立した型として扱うときの T
を 非変 という。
Scalaでは型パラメータは基本的に非変である。そのためエラーとなったのである。
ところで、ここで Max[T]
を返すことに何か問題はあるだろうか。
T
については下限境界を設定しているため、 Countable
の性質を備えていることが自明であるため、 Max[T]
が Max[Countable]
のサブ型とみて問題はないだろう。
そのため、ここでは型パラメータを共変に変更することで Max[T] extends Max[Countable]
であることを明示する。そうすれば、 Max[Countable]
を要求する場面で、より具体的な Max[T]
を返却することができるようになる。
- case class Max[T <: Countable](initial: T) {
+ case class Max[+T <: Countable](initial: T) { // + が共変の指定
val value = initial
def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
}
これで、 max
の結果は常に Max[Countable]
として得られるのである。
max
をもっと便利に
下限境界を使ってmaxにはもう少し改善の余地がある。
val max = Max(Shogi).max(Mahjong)
上記の変数 max
の型はどうなるだろうか。
Shogi
と Mahjong
しか渡していないため、結果は Max[Shogi]
か Max[Mahjong]
のどちらかにしかならないはずである。
つまり、 Max[TableGame]
であることは確実である。
ところが、実際には Max[Countable]
になってしまう。
いつでもCountable
というのは型を決めすぎていて少し扱いが難しい。
今回の例で言えば Max[TableGame]
として取り扱いたいところである。
これは新たな型パラメータと 下限境界 を導入することで改善可能である。
case class Max[+T <: Countable](initial: T) {
val value = initial
- def max(x: Countable): Max[Countable] = if (x.number > value.number) Max(x) else this
+ def max[U >: T <: Countable](x: U): Max[U] = if (x.number > value.number) Max(x) else this // >: が下限境界の指定
}
少しややこしいが、 U >: T <: Countable
と書くことで、この U
の取るクラスの継承関係が下図の範囲であることを示している。
この状態で次のコードを実行すると、 TableGame
として扱えていることがわかる。
val max = Max(Shogi).max(Mahjong)
println(s"${max.value.description}は${max.value.number}です。プレイヤー数は${max.value.players}です。")
// ==> "麻雀牌の数は136です。プレイヤー数は4です。"
反変(contravariant)
今回は取り上げなかったが、非変、共変の他に 反変 というものもある。
B extends A
であるとき、 C[T]
について C[A] extends C[B]
を満たすときの T
を反変という。
反変の指定は -
で行う。
trait Channel[-T] {
def output(x: T)
}
抽出子
unapply(a: T): Option[U]
を実装したオブジェクトを抽出子と呼ぶ。
基本的にコンストラクタパターンにおけるパターンマッチはケースクラスに対して行うものであるが、抽出子を実装することで、ケースクラスでないオブジェクト(上記の例では数値)に対してもパターンマッチを適用することができる。
下記は1~100までの中で平方数を探すプログラムである。
import scala.math.sqrt
object Square {
def apply(num: Int) = num * num
def unapply(num: Int): Option[Int] = {
if (sqrt(num) % 1 == 0) Some(sqrt(num).toInt) else None
}
}
for (i <- 1 to 100) i match {
case Square(x) => println(s"$i is a square number of $x.")
case _ => println(s"$i is not a square number.")
}
返す値の数が可変長になる場合を考慮して、unapplySeq(a: T): Option[Seq[U]]
というものも用意されている。
下記はタプル2から公約数を求めるプログラムである。
import scala.math._
object CommonDivisor {
def unapplySeq(num: (Int, Int)): Option[Seq[Int]] = {
val gcd = grand(num._1, num._2)
if (gcd == 1) {
Some(Seq(1))
} else {
val divisorsOfGcd = for {
i <- 1 to ceil(sqrt(gcd)).toInt
if gcd % i == 0
} yield {
if (i > 1 && i < sqrt(gcd)) Seq(i, gcd / i) else Seq(i)
}
Some(divisorsOfGcd.flatten.sorted :+ gcd)
}
}
private def grand(a: Int, b: Int): Int = {
val (higher, lower) = (max(a, b), min(a, b))
if (lower == 0) higher else grand(lower, higher % lower)
}
}
(120, 80) match {
case n @ CommonDivisor(m @ _*) => println(s"All common divisors of $n are ${m.mkString(", ")}.")
}
// ⇒ "All common divisors of (120,80) are 1, 2, 4, 5, 8, 10, 20, 40."