1. 概要

このチュートリアルでは、Scalaの分散を見ていきます。 Varianceは、型コンストラクター(Javaの汎用型と同等)が別の型コンストラクターのサブタイプであるかどうかを示します。

また、分散の3つの主要なタイプ(不変、共分散、反変性)と、それらの違いについても見ていきます。

2. サブタイピングと型構築子

すべてのプログラミング言語は、型の概念をサポートしています。 タイプは、実行時に値を処理する方法に関する情報をプログラムに提供します。 サブタイピングにより、型の値にさらに制約が追加されます。

単純なタイプの場合、話は簡単です。

sealed trait Test 
class UnitTest extends Test 
class IntegrationTest extends UnitTest 
class FunctionalTest extends IntegrationTest

タイプFunctionalTestは、クラスUnitTest。のサブタイプであるIntegrationTestのサブタイプです。

さらに、多くのプログラミング言語は、汎用型または型構築子もサポートしています。 タイプコンストラクターは、古いタイプから始めて新しいタイプを作成するメカニズムです。 これらは、具象型にバインドできる型変数を提供します。

ジェネリック型Tのオブジェクトに対するテストの結果をモデル化するとします。 型コンストラクターを使用して、このような状況をモデル化できます。

class TestResult[T](id: String, target: T) {
  // Class behavior
}

Varianceは、型コンストラクター間のサブタイピング関係を定義します。は、型変数をバインドする型間のサブタイピング関係を使用します。 言い換えると、型コンストラクター F [_] の場合、BAのサブタイプである場合、分散は型F間の関係を表します。 [B]およびタイプF[A]。

3. 分散

分散には、共分散、反変性、不変の3つのタイプがあります。 それぞれを詳しく見ていきましょう。

3.1. 共分散

共分散は、非常に簡単に理解できる概念です。 BがタイプAのサブタイプであり、 F [B]がタイプF[A]のサブタイプ。 Scalaでは、表記 F [+ T] を使用して共変型コンストラクターを宣言し、型変数の左側にプラス記号を追加します。

前のセクションのTestタイプの階層について考えてみます。 タイプUnitTestが階層のベースにあると想定しており、各ステップで、より洗練された進化した機能を追加しています。 多くの場合、単独でだけでなく、スイートでテストを実行する必要があります。 テストスイートは、テストのリストにすぎません。

class TestsSuite[+T](tests: List[T])

タイプコンストラクターTestsSuiteを共変として定義しました。これは、タイプ TestsSuite[IntegrationTest]TestsSuite[UnitTest]のサブタイプであることを意味します。 共分散プロパティを使用すると、次のような変数を宣言できます。

val suite: TestsSuite[Test] = new TestsSuite[UnitTest](List(new UnitTest))

タイプTestsSuite[T] の変数を割り当てる必要があるたびに、 RTのサブタイプ。 この場合、共分散は、サブタイピングの標準的な動作を反映しているため、タイプセーフです。 そのスーパータイプの1つの変数にオブジェクトを割り当てることは、常に安全です。

型コンストラクターTestsSuite[T] から共変アノテーションを削除すると、コンパイラーは、上記の例では型UnitTestのオブジェクトを使用できないことを警告します。

type mismatch;
 found   : com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.UnitTest]
 required: com.baeldung.scala.variance.Variance.TestsSuits[com.baeldung.scala.variance.Variance.Test]
Note: com.baeldung.scala.variance.Variance.UnitTest <: com.baeldung.scala.variance.Variance.Test, but class TestsSuits is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
  val suite: TestsSuits[Test] = new TestsSuits[UnitTest](List(unitTest))
                                ^

Scala SDKには、型変数に関して共変として宣言された型コンストラクターの例がたくさんあります。 より人気のあるものは、いくつか例を挙げると、 List [T] Option [T] 、および Try[T]です。

3.2. 共変性

BがタイプAのサブタイプであり、 F [A]がタイプF[B]のサブタイプ。 この関係は、共分散関係とは正反対です。 Scalaでは、 F [-T] という表記を使用して、型変数の左側にマイナス記号を追加して、反変型コンストラクターを宣言します。

一見すると、反変性は直感に反しているように見えるかもしれません。 型構築子のためにこのタイプの関係が必要なのはなぜですか? 従業員のドメインモデルをモデル化して、クラスの階層を定義しましょう。

class Person(val name: String)
class Employee(name: String, val salary: Int) extends Person(name)
class Manager(name: String, salary: Int, val manages: List[Employee]) extends Employee(name, salary)

テストアサーションを表す型コンストラクターを定義できます。

class Assert[-T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

タイプAssertのインスタンスは、汎用タイプTからブールまでの関数です。 一部のプロパティがタイプのオブジェクトを保持していることを確認する必要があります T、 と呼ばれる目標。

従業員階層のAssertのリストは、クラスの属性の条件を検証します。

val personAssert = new Assert[Person](p => p.name == "Alice")
val employeeAssert = new Assert[Employee](e => e.name == "Bob" && e.salary) 
val managerAssert = new Assert[Manager](m => m.manages.nonEmpty)

同じターゲットオブジェクトで複数のAssertをテストすることは可能です。 集約タイプAssertsを定義できます。

trait Asserts[T] {
  def asserts: List[Assert[T]]
  def execute(target: T): Boolean =
    asserts
      .map(a => a.assert(target))
      .reduce(_ && _)
}

型コンストラクターAssertsは、同じtargetオブジェクトでexecuteするAssertのリストです。 AssertsEmployeeタイプに特化して、Employeeインスタンスで実行できるAssertのリストを取得できます。

class AssertsEmployee(val asserts: List[Assert[Employee]]) extends Asserts[Employee]

従業員でどのようなアサートをテストできますか? 型コンストラクターAssertは反変として定義されています。つまり、 Assert[Person]は実際にAssert[Employee]のサブタイプです。 したがって、 Assert のリストには、 Assert[Employee]またはAssert[Person]のいずれかのインスタンスを含めることができます。

val bob = new Employee("Bob", 50000) 
val tester = new AssertsEmployee(List(personAssert, employeeAssert)) 
tester.execute(bob)

タイプEmployeeのオブジェクトに対してAssert[Empoyee] を実行し、属性nameと属性salaryテストできます。 タイプEmployeeのオブジェクトに対してAssert[Person]を実行することもできます。 この場合、アサーションはEmployeeが所有するプロパティnameのみをテストできます。

Assert 型コンストラクターの定義から共変性アノテーションを削除しようとすると、コンパイラーは何かが欠落していることを警告します。

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
Note: com.baeldung.scala.variance.Variance.Person >: com.baeldung.scala.variance.Variance.Employee, but class Assert is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
  val tester = new AssertsEmployee(List(personAssert, employeeAssert))
                                        ^

タイプAssert[Manager] はどうですか? マネージャーのアサートは、タイプEmployeeのオブジェクトが持っていない属性managesをテストできます。

val tester = new AssertsEmployee(List(managerAssert))
tester.execute(bob)

したがって、コンパイラは、タイプ Assert[Manager]のオブジェクトを使用できないことを警告します。

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Manager]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
  val tester = new AssertsEmployee(List(managerAssert))
                                      ^

Scala SDKで、最も人気のある反変型コンストラクターは Function1 [-T1、+R]。 型コンストラクターは、型の1つのパラメーターを持つ関数を表します T1 。 すでに見てきたように、関数またはメソッドへの入力パラメーターとして型変数を使用すると、共変性が助けになり、型セーフコードを定義できるようになります。

3.3. 不変性

型コンストラクターとその型変数の間の3番目のタイプの関係は、不変性です。 タイプABの間のサブタイプ関係がタイプ間で任意の順序で保持されない場合、タイプコンストラクター F[_]は不変であると言います F[A]およびF[B]。

前のAssertタイプから反変性プロパティ(-)を削除すると、不変タイプコンストラクターが得られます。

class Assert[T](expr: T => Boolean) {
  def assert(target: T): Boolean = expr(target)
}

タイプAssert[Person]の変数をタイプAssert[Employee]のオブジェクトに割り当ててみましょう。

val personAssert: Assert[Person] = new Assert[Employee](p => p.name == "Alice")

Tの不変性により、コンパイラは正しく警告します。

type mismatch;
 found   : com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Employee]
 required: com.baeldung.scala.variance.Variance.Assert[com.baeldung.scala.variance.Variance.Person]
Note: com.baeldung.scala.variance.Variance.Employee <: com.baeldung.scala.variance.Variance.Person, but class Assert is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
  val personAssert: Assert[Person] = new Assert[Employee](p => p.name == "Alice")
                                     ^

不変性は、JavaやC ++と同様に、型コンストラクターの使用を可能にする多くのプログラミング言語で使用可能な唯一の型の関係です。

4. 結論

この記事では、型構築子またはジェネリック型の概念を紹介しました。 単純型に対して定義されたサブタイピングの概念から始めて、分散の定義を通じて、サブタイプの概念が型構築子にどのように変換されるかを示しました。

共分散、反変性、不変の3種類の分散を調べました。 ジェネリック型変数の代入を使用する場合は共分散を使用する必要があり、引数としてジェネリック型を使用する場合は反変性を使用する必要があると結論付けました。

いつものように、この記事のコードはGitHubにあります。