Scalaのジェネリックスの基礎
1. 概要
このチュートリアルでは、コンテナーの実装におけるScalaジェネリックスの利点を見ていきます。 ScalaジェネリックがDRYの原則を順守するのを助けながら、どのように型安全性を提供するかを調べます。
ジェネリッククラスとメソッドを作成する手順を実行し、標準のScalaライブラリで利用可能なジェネリック型を調べます。
2. コンテナクラス
汎用クラスを使用するための最も一般的なシナリオはcontainersです。
マジシャンの帽子のクラスを設計しているとしましょう。 それはかわいいバニーまたはちょうどリンゴまたは他のものを持つことができます。 これにアプローチする方法はいくつかあります。
2.1. 特定のタイプの場合
予想通りに使用できるように、コンパイラーに帽子に含まれる魔法のオブジェクトのタイプを保証してもらいたいと思います。 興味のあるタイプに固有のクラスを書くことができます。
case class AppleMagicHat(magic: Apple)
def run(): Unit = {
val someHat = AppleMagicHat(Apple("gala"))
val apple: Apple = someHat.magic
println(apple.name)
}
これで、コンパイラは、someHatの魔法のアイテムがAppleインスタンスにしかなり得ないことを保証します。
しかし、Rabbitというタイプのオブジェクトもあるとしたらどうでしょうか。
case class Rabbit(cuteness: Int)
Rabbit インスタンスを提供するマジックハットを作成するには、Rabbitインスタンスで動作するRabbitMagicHatという別のクラスを作成する必要があります。 このアプローチでは、
これは、DRYの原則に反しています。 それでは、1つのコンテナクラスだけを使用してみましょう。
2.2. 型安全性なし
あらゆる種類の魔法のオブジェクトを保持できる単一の魔法の帽子のクラスを持つことができます。
case class MagicHat(magic: AnyRef)
val someHat = MagicHat(Rabbit(2))
val apple: Apple = someHat.magic.asInstanceOf[Apple]
println(apple.name)
帽子の中のmagicアイテムにAnyRefを使用する必要がある理由は、複数のタイプにすることができるためです。 Scalaでは、すべてのオブジェクトはAnyRefから継承します。
ただし、これはエラーが発生しやすいです。 ここでは、帽子の中のRabbitを誤ってAppleインスタンスとして扱ったときに、4行目にキャスト例外がスローされていることがわかります。
3. 一般的なクラスによる型安全性
Scalaでクラスを宣言するときに、型パラメーターを指定できます。 これらのタイプパラメータを囲むために角かっこを使用します。
たとえば、 class Foo[A]を宣言できます。 次に、プレースホルダー A をクラスの本体で使用して、タイプを参照できます。 ジェネリッククラスを使用する場合、コードはそれぞれの場合に使用するタイプを指定します。
値パラメーターとは異なり、タイプパラメーターが1文字を超えることはめったにありません。慣例により、Aで始まります。
2つのパラメーターの例は、 class Bar [A、B]のようなものです。
タイプパラメータを使用する利点は、魔法の帽子の中の物のタイプがわかるようになったことです。 コンパイラはタイプをチェックでき、正しいタイプを追加しないと失敗します。
case class MagicHat[A](magic: A)
val rabbitHat = MagicHat[Rabbit](Rabbit(2))
val rabbit: Rabbit = rabbitHat.magic
println(rabbit.cuteness)
ここに型パラメーターAがあり、3行目のcaseクラスコンストラクターの呼び出し元は、型がRabbitであることを指定しています。
現在、 MagicHat [Rabbit] は、特別なRabbitMagicHatクラスを作成した場合と同じタイプセーフティを提供します。 そして、MagicHatクラスを1つだけ宣言することでこれを達成しました。 また、参照をキャストする必要はありません。
4. Scala標準ライブラリの例
ジェネリッククラスのユースケースを理解するための最良の方法の1つは、Scala標準ライブラリの例を調べることです。
ほとんどのScala汎用クラスはコレクションであり、不変の List 、 Queue 、 Set 、 Map 、それらの可変の同等物、およびスタック。
コレクションは、0個以上のオブジェクトのコンテナです。 また、最初はそれほど明白ではない汎用コンテナもあります。 たとえば、 Option は、0個のオブジェクトまたは1個のオブジェクトを受け取ることができるコンテナです。 Try は、値またはThrowableのいずれかを含むコンテナーです。 また、 Future は、アクションが完了したときの値の可能性を表します。
5. 一般的な方法
一般的な入力を受け取る、または一般的な戻り値を生成するScalaメソッドを作成する場合、メソッド自体が一般的である場合とそうでない場合があります。
5.1. 宣言構文
ジェネリックメソッドの宣言は、ジェネリッククラスの宣言と非常によく似ています。 まだタイプパラメータを角かっこで囲んでいます。
また、メソッドの戻りタイプもパラメーター化できます。
def middle[A](input: Seq[A]): A = input(input.size / 2)
このメソッドは、選択したタイプのアイテムを含む Seq を受け取り、Seqの途中からアイテムを返します。
val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7))
val middleRabbit: Rabbit = middle[Rabbit](rabbits)
次に、複数のタイプパラメータを使用した例を見てみましょう。
def itemsAt[A, B](index: Int, seq1: Seq[A], seq2: Seq[B]): (A, B) = (seq1(index), seq2(index))
このメソッドは、両方のSeqのindexにある要素を受け取り、入力で使用されるタイプに一致するタプルを返します。
val apples = List[Apple](Apple("gala"), Apple("pink lady"))
val items: (Rabbit, Apple) = itemsAt[Rabbit, Apple](1, rabbits, apples)
上記の例は、空の Seq などのエッジケースを正しく処理しないため、本番環境に対応していないことに注意してください。 ただし、これらは、型パラメーターが引数の型と戻り値の型を強制するのにどのように役立つかを示しています。
5.2. 非ジェネリックメソッドでのジェネリッククラスの使用
ジェネリッククラスを使用する場合、必ずしもジェネリックメソッドを作成するわけではありません。 たとえば、任意のタイプの2つの List を取り、全長を返すメソッドは、次のように宣言できます。
def totalSize(list1: List[_], list2: List[_]): Int
_ (アンダースコア)は、リストの内容は関係ないことを意味します。 タイプパラメータがないため、呼び出し元がこのメソッドのタイプを提供する必要はありません。
val rabbits = List[Rabbit](Rabbit(2), Rabbit(3), Rabbit(7))
val strings = List("a", "b")
val size: Int = totalSize(rabbits, strings)
この例では、関数が呼び出されたときに型が関数に提供されていません。
6. アッパータイプバウンド
Scalaの型の上限を説明するために、コレクション内の最大要素を見つけるための基本的でありながら総称的な関数を記述して、車輪の再発明を行いましょう。 これが私たちの最初の試みです:
def findMax[T](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)
ロジックは問題ないように見えますが、>=関数は汎用タイプTで定義されていません。 したがって、findMax関数はまったくコンパイルされません。
Ordered[T]がそのような比較関数のホームであることを私たちは知っています。 したがって、この例では、TがOrdered[T]のサブタイプであることをコンパイラに通知する必要があります。
結局のところ、Scalaジェネリックスの上限タイプは私たちのためにこれを行います:
def findMax[T <: Ordered[T]](xs: List[T]): Option[T] = xs.reduceOption((x1, x2) => if (x1 >= x2) x1 else x2)
7. 下限タイプの境界
Programming inScalaの本から例を借りてみましょう。
class Queue[+T](private val leading: List[T], trailing: List[T]) {
def head(): T = // returns the first element
def tail(): List[T] = // everything but the first element
def enqueue(x: T): Queue[T] = // appending to the end
}
ここでは、2つのリストでキューを表現しようとしています。
ここで、 Queue[String]を操作する必要があるとします。 欲しいのでキュー[文字列] のサブタイプになるキュー[任意] 、共変型アノテーションを使用しました
covariant type T occurs in contravariant position in type T of value x
def enqueue(x: T): Queue[T] = new Queue(leading, x :: trailing)
型パラメーター[+T] は共変ですが、反変の位置(関数の引数)で使用したため、コンパイラーはそれについて文句を言います。
この問題を修正する1つの方法は、下限タイプを使用することです。
def enqueue[U >: T](x: U): Queue[U] = new Queue(leading, x :: trailing)
val empty = new Queue[String](Nil, Nil)
val stringQ: Queue[String] = empty.enqueue("The answer")
val intQ: Queue[Any] = stringQ.enqueue(42)
IntをQueue[String] に追加すると、コンパイラはStringとIntに最も近いスーパータイプを自動的に推測します。 したがって、返されるタイプは Queue[Any]です。
8. 結論
この記事では、汎用クラスを使用すると、含まれる型ごとに新しいコンテナークラスを宣言する必要がなく、型の安全性を実現できることを示しました。
ジェネリッククラスを宣言する手順を見て、標準のScalaライブラリの例を訪れました。
最後に、ジェネリックメソッドと非ジェネリックメソッドの両方を宣言する際の違いもわかりました。
いつものように、この記事で使用されているサンプルコードは、GitHubのにあります。