Closed3

Scala学習メモ

tkzwhrtkzwhr

等価性

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. */
}
  1. Any#equals をオーバーライドする
    • 引数の型はクラスの型ではなく Any になる
  2. hashCode は常に equals で使っている値をすべて使って常に同じ計算結果となるようにする
    • ##で簡単にハッシュ値を作れる
  3. ミュータブルなプロパティを equals に使用してはいけない
  4. 下記条件を満たすようにする
    • 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
  5. 冪等である

継承関係にあるクラスでのポイント

継承関係にあるクラスでの等価性は、基本的に 厳格 にしたほうが良い場合が多い。

例えば、下記のような TextAttributedText のクラスでは、比較結果が 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
tkzwhrtkzwhr

変位パラメータ

前提

前提として、計数可能な性質を表す 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
}

ただし、このままだと TCountable を継承しない型をとれるため、 max でコンパイルエラーとなってしまう。

value number is not a member of type parameter T

そこで、 TCountable の性質をもつことを示すため、 上限境界 を設定する。

- 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
  }

こうすることで、 TCountable のサブクラスのインスタンスまたはミックスインされたオブジェクトに限定することができるため、 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 の型はどうなるだろうか。
ShogiMahjong しか渡していないため、結果は 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)
}
tkzwhrtkzwhr

抽出子

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."
このスクラップは2023/10/07にクローズされました