1. 概要

このチュートリアルでは、Scalaで TypeDisjunctionまたはUnionTypesを表現する方法を学習します。

Scala3を除いて、Scalaには組み込みの共用体タイプはありません。 ただし、EitherやScalaのtypeclassesなどのScalaの言語機能を使用して、共用体型の機能を表すことができます。

関連する例を使用して、それらのそれぞれを詳細に調べます。

2. 共用体の種類

名前が示すように、共用体タイプは、2つ以上のタイプの共用体であるタイプを示します。 たとえば、IntおよびString値の共用体型を定義し、整数値または文字列値のいずれかを入力として期待する関数のパラメーターとして使用できます。

Scalaは強力で静的に型付けされた言語です。したがって、コードのコンパイル時にパラメーター型と戻り型を定義する必要があります。 これにより、さまざまなタイプのパラメーターを受け入れる関数の柔軟性が低下します。 たとえば、String値をIntパラメーターを受け入れる関数に渡すことはできません。

このような問題に対処する従来の方法は、オーバーロードされた関数を定義することです。 ただし、これが常に最も単純なアプローチであるとは限りません。 ユニオン内のタイプの数が増えると、オーバーロードが複雑になり、管理が難しくなる可能性があります。 共用体タイプは、このような問題に対する最も適切な解決策です。

3. どちらかアプローチ

Scala標準ライブラリは、2つのタイプの非交和を表すために使用されるEitherタイプを提供しますどちらかは、通常、オプションタイプと同様のユースケースに使用されます。 Right はハッピーパスを示し、Leftは例外ケースを示します。 Scalaバージョン2.12以降、どちらも右バイアスであるため、モナドとしても使用できます。

どちらかを使用して、2つのタイプの非交和型を定義できます。 タイプがIntまたはStringのいずれかであるパラメーターを受け取り、パラメータータイプを出力する関数を作成してみましょう。

def isIntOrString(t: Either[Int, String]): String ={
  t match {
    case Left(i) => "%d is an Integer".format(i)
    case Right(s) => "%s is a String".format(s)
  }
}

println(isIntOrString(Left(10))) // prints "10 is an Integer"
println(isIntOrString(Right("hello"))) // prints "hello is a String"

非交和にEitherタイプを使用する場合の主な制限は、2つのタイプにしか使用できないことです。 どちらのもボックス化されたタイプであるため、値をRightまたはLeftのインスタンスとしてラップする必要があります。

4. 任意-アリティユニオンタイプ

これまで、2つの型だけで構成される共用体型を作成する方法を見てきました。 3つ以上の型の共用体型を作成したい場合はどうなりますか? これに対処する1つの方法は、ネストを行うことです。 ネスティングはすぐに複雑になり、保守が面倒になります。

幸い、typeclassesimplicitsを使用して、Scalaで共用体型を定義するための別のソリューションがあります。

Integer String 、および Boolean の3つの型の共用体型の型クラスを、封印された特性として宣言することから始めましょう。タイプクラスへの不要な拡張を防ぐことができます。

sealed trait IntOrStringOrBool[T]
object IntOrStringOrBool {
  implicit val intInstance: IntOrStringOrBool[Int] =
    new IntOrStringOrBool[Int] {}
  implicit val strInstance: IntOrStringOrBool[String] =
    new IntOrStringOrBool[String] {}
  implicit val boolInstance: IntOrStringOrBool[Boolean] =
    new IntOrStringOrBool[Boolean] {}
}

次に、新しく作成した共用体型を取得し、単純なパターンマッチングを使用して型を抽出する関数を宣言しましょう。

def isIntOrStringOrBool[T: IntOrStringOrBool](t: T): String = t match {
  case i: Int => "%d is an Integer".format(i)
  case s: String => "%s is a String".format(s)
  case b: Boolean => "%b a Boolean".format(b)
}

次に、さまざまなタイプの値を使用して関数を呼び出すことができます。

println(isIntOrStringOrBool(10)) // prints "10 is an Integer"
println(isIntOrStringOrBool("hello")) // prints "hello is a String"
println(isIntOrStringOrBool(true)) // prints "true is a Boolean"

型クラスに別の暗黙のインスタンスを追加するだけで、共用体型を拡張してもう1つの型を含めることが簡単になりました。

ジェネリック型を使用して型を抽象化することにより、上記のアプローチをさらに一般化できます。

sealed trait AOrB[A, B]
object AOrB {
  implicit def aInstance[A,B](a: A) = new AOrB[A, B] {}
  implicit def bInstance[A,B](b: B) = new AOrB[A, B] {}
}

def isIntOrString[T <% String AOrB Int](t: T): String = t match {
  case i: Int => "%d is an Integer".format(i)
  case s: String => "%s is a String".format(s)
}

5. Scala3の共用体タイプ(Dotty)

タイプ交差はwith演算子を使用する標準のScalaライブラリに存在しますが、ユニオン演算子はありません。 Scala3( Dotty プロジェクト)は、新しいものを導入することによってこれに対処しようとしています演算子を使用して表される共用体タイプと交差タイプ| と &

これらを使用して、結合された共用体タイプと分離された共用体タイプの両方を定義できます。 Scala3共用体タイプには、両方のタイプのすべての値が含まれます。 どちらかとは異なり、共用体タイプはボックス化されておらず、任意のアリティもあります。

Scala3共用体型演算子を使用して、上記の問題の解決策を書いてみましょう。

def isIntOrString(t:Int|String|Boolean)=t match {
  case i: Int => "%d is an Integer".format(i)
  case s: String => "%s is a String".format(s)
  case b: Boolean => "%b is a Boolean".format(b)
}

println(isIntOrString(10))  //prints "10 is an Integer"
println(isIntOrString("hello")) // prints "hello is a String"
println(isIntOrString(true)) // prints "true is a Boolean"

6. 結論

このチュートリアルでは、 と型クラスのいずれかを使用して、Scalaで共用体型を実装するさまざまな方法を学びました。

最初に、例を使用して、 Either タイプを使用して、2つのタイプの共用体タイプを作成する方法と、その長所と短所を学びました。

次に、 Either アプローチの制限を克服するために、型クラスと暗黙を使用して共用体型を定義できることを確認しました。

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