1. 概要

列挙型は、名前付き値の有限セットのみを含むデータ型であり、これらはほとんどの最新のプログラミング言語でサポートされています。

この機能は通常、列挙型または列挙型と呼ばれます。 そして、それは私たちがそのすべての可能な要素をリストすることによってタイプを宣言することを可能にします。

Scalaはマルチパラダイム言語であるため、列挙型を作成する2つの方法から継承されます。

列挙型は純粋にオブジェクト指向ですが、代数的データ型は関数型プログラミングに合わせて調整されています。

質問を提起します。いつ、なぜどちらを使用する必要がありますか?

2. Scala列挙の要約

Scala列挙の使い方は簡単です。

  • 列挙クラスを拡張します
  • 序数(整数)と名前(文字列)を提供する値として要素を宣言します
object CurrencyEnum extends Enumeration {
  type Currency = Value
  
  val GBP = Value(1, "GBP")
  val EUR = Value
}

序数や名前を指定しない場合、Scalaコンパイラーがデフォルトを割り当てます。 Scalaは使用された番号を追跡し、次の番号を割り当てます。明示的な入力を行わない場合は、ゼロから始まります。 対照的に、文字列はデフォルトで変数の名前です。

残念ながら、列挙には2つの大きな欠点があります

  • 型消去のため、すべての列挙型は実行時に同じ型になります
  • また、継承が制限されていないため、コンパイラは不完全なパターン一致を検出できません

もう1つの小さな問題は、より多くのデータを保持するために要素を拡張することが難しいことです。

3. タイプセーフな代替案

代数的データ型は、各要素の形状を指定するものです。したがって、列挙型と同じユースケースでそれらを適用できます。

Scalaは、製品タイプや合計タイプなどの代数的データ型のエンコードをサポートしています。 列挙をエンコードするには、合計タイプのみが必要です。

Scala 2では、sumタイプは、封印されたトレイトまたは抽象クラスとケースオブジェクトを使用して記述されます。

sealed abstract class CurrencyADT(name: String, iso: String)

object CurrencyADT {
  case object EUR extends CurrencyADT("Euro", "EUR")
  case object USD extends CurrencyADT("United States Dollar", "USD")
}

列挙を拡張する場合と比較した場合のこのアプローチの欠点は、要素を反復処理したり、文字列から要素を取得したりする方法が自動的に取得されないことです。

したがって、次のような通貨ADTのコードを記述することはできません。

import CurrencyEnum._ 

println(EUR) 
println(CurrencyEnum.withName("GBP")) 
for (cur <- CurrencyEnum.values) {
  println(cur)
}

3.1. 代数的データ型が優れているのはなぜですか?

ADTは、列挙で識別される2つの主要な問題を解決します。

シーリング基本クラス/特性は、同じファイル内のコードのみがそれらを拡張できることを意味します; したがって、コンパイラはパターンの一致が徹底的にチェックできるため、1つのクラスのランタイムエラーが排除されます。

caseオブジェクトを使用すると、equals、hashCode、およびtoStringメソッドのデフォルトが改善されるようにコンパイラーに指示されます。デフォルトの toString メソッドは、オブジェクトの名前を含む文字列を返すため、適切に適合します。列挙を実装します。

これらの機能は直交しています。 言い換えれば、それらを独立して使用することができ、それらの効果は補完的です。

abstract class CurrencyADT(val name: String, val iso: String)

object CurrencyADT {
  case object EUR extends CurrencyADT("Euro", "EUR")
  case object USD extends CurrencyADT("United States Dollar", "USD")
}

上記の例では、完全性チェックを除いて、ほとんどの利点を提供するために、封印されていない基本クラスを持つケースオブジェクトを使用しました。

一方、通常のオブジェクトで封印された基本クラスを使用して、網羅性チェックを取得し、独自の toString hashCode 、およびequals実装を提供することもできます。

3.2. ADTの欠点の修正

withName 関数は便利ですが、大きな問題があります。 これは安全ではなく、 NoSuchElementException をスローする可能性があります。これは、一部の命令型コードでは許容できる場合がありますが、機能的なコードベースでの使用が困難になります。

ADTの純粋なバージョンを作成できます。

def fromIso(iso: String): Option[CurrencyADT] = {
  iso.toUpperCase match {
    case "EUR" => Some(EUR)
    case "USD" => Some(USD)
    case _ => None  
  }
}

列挙型のすべての値を反復処理することは一般的なユースケースではありませんが、本当に必要な場合は、次のように簡単に記述できます。

object CurrencyADT { 
    // Previous code omitted for conciseness val values: 
    Seq[CurrencyADT] = Seq(EUR, USD) 
}

列挙型によっては、 Map などの別のコレクションタイプを使用することで、欠点を利点に変換できます。また、それを使用して、解析方法を簡略化することもできます。

val isoToCurrency: Map[String, CurrencyADT] = values.map(c => c.iso -> c).toMap

def fromIso(iso: String): Option[CurrencyADT] = isoToCurrency.get(iso.toUpperCase)

ADTの欠点を克服するためのコードを書くのは面倒ですが、それでもバランスをとると、ADTアプローチが最良の選択肢であると結論付けることができます。

4. 未来への展望

Scalaの列挙はJavaと互換性がありません。 つまり、Javaコードは、エンコードするために選択したアプローチに関係なく、Scalaで宣言された列挙型を使用できません。

Scala 3は、ADTと列挙型を新しい構文で統合することでこれらの問題を解決します。これは、オプションでJava列挙型と互換性を持たせることができます。

object CurrencyADT(name: String, iso: String) extends java.lang.Enum {
    case EUR("Euro", "EUR")
    case USD("United States Dollar", "USD")
}

この新しい構文は、Scala 2の両方の列挙アプローチに関するすべての問題を解決し、Java互換性を提供します。

5. 結論

このチュートリアルでは、Scala 2で列挙型を作成するためのオプションを確認し、アプローチの長所と短所の両方を理解しました。

また、将来を掘り下げて、Scala 3がADTと列挙型を統合して、両方の機能の単一の構文を提供する方法のプレビューを見ました。

いつものように、例の完全なソースコードは、GitHubから入手できます。