web-dev-qa-db-ja.com

Scalaのケースオブジェクトと列挙

ケースクラス (またはケースオブジェクト)とScalaの列挙の拡張を使用する場合のベストプラクティスガイドラインはありますか?

彼らは同じ利点のいくつかを提供するようです。

225
Alex Miller

大きな違いの1つは、Enumerationsがname文字列からインスタンス化するためのサポートを備えていることです。例えば:

object Currency extends Enumeration {
   val GBP = Value("GBP")
   val EUR = Value("EUR") //etc.
} 

その後、次のことができます。

val ccy = Currency.withName("EUR")

これは、列挙(データベースなど)を永続化する場合、またはファイルにあるデータから列挙を作成する場合に便利です。しかし、一般的に、列挙はScalaで少し不器用であり、厄介なアドオンの感触があるため、case objectsを使用する傾向があります。 case objectは列挙型よりも柔軟性があります。

sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.

case class UnknownCurrency(name: String) extends Currency

だから今私はの利点を持っています...

trade.ccy match {
  case EUR                   =>
  case UnknownCurrency(code) =>
}

@ chaotic3quilibrium が指摘されている(読みやすくするためにいくつかの修正が加えられている):

「UnknownCurrency(code)」パターンについては、Currencyタイプのクローズドセットの性質を「破る」以外に、通貨コード文字列を見つけないように処理する方法が他にもあります。 UnknownCurrency型であるCurrencyは、APIの他の部分に潜入できるようになりました。

そのケースをEnumerationの外にプッシュし、クライアントにOption[Currency]タイプを処理させることをお勧めします。彼女自身。

ここで他の回答をフォローアップするために、Enumerationsよりもcase objectsの主な欠点は次のとおりです。

  1. 「列挙」のすべてのインスタンスを反復処理することはできません。確かにそうですが、実際にはこれが必要になることは非常にまれであることがわかりました。

  2. 永続化された値から簡単にインスタンス化できません。これも事実ですが、巨大な列挙(たとえば、すべての通貨)の場合を除き、これは大きなオーバーヘッドを与えません。

218
oxbow_lakes

ケースオブジェクトはすでにtoStringメソッドの名前を返しているため、個別に渡す必要はありません。これは、jhoに似たバージョンです(簡潔にするために、便宜上の方法は省略されています)。

trait Enum[A] {
  trait Value { self: A => }
  val values: List[A]
}

sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
  case object EUR extends Currency
  case object GBP extends Currency
  val values = List(EUR, GBP)
}

オブジェクトは怠zyです。代わりにvalsを使用すると、リストを削除できますが、名前を繰り返す必要があります。

trait Enum[A <: {def name: String}] {
  trait Value { self: A =>
    _values :+= this
  }
  private var _values = List.empty[A]
  def values = _values
}

sealed abstract class Currency(val name: String) extends Currency.Value
object Currency extends Enum[Currency] {
  val EUR = new Currency("EUR") {}
  val GBP = new Currency("GBP") {}
}

不正行為を気にしない場合は、リフレクションAPIまたはGoogle Reflectionsなどを使用して列挙値をプリロードできます。非遅延ケースオブジェクトは、最もクリーンな構文を提供します。

trait Enum[A] {
  trait Value { self: A =>
    _values :+= this
  }
  private var _values = List.empty[A]
  def values = _values
}

sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
  case object EUR extends Currency
  case object GBP extends Currency
}

きれいで、ケースクラスとJava列挙のすべての利点があります。個人的には、イディオムのScalaコードによりよく一致するように、オブジェクトの外側で列挙値を定義します。

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
63
GatesDA

列挙よりもケースクラスを使用する利点は次のとおりです。

  • シールドケースクラスを使用する場合、Scalaコンパイラは、一致が完全に指定されているかどうかを判断できます。一致する可能性のあるすべての一致が一致宣言で支持される場合。列挙型では、Scalaコンパイラーは認識できません。
  • ケースクラスは、当然、名前とIDをサポートする値ベースの列挙よりも多くのフィールドをサポートします。

ケースクラスの代わりに列挙を使用する利点は次のとおりです。

  • 列挙型は一般に、記述するコードが少し少なくなります。
  • 列挙は、他の言語で普及しているため、Scalaを初めて使用する人にとっては少しわかりやすい

したがって、一般に、名前による単純な定数のリストだけが必要な場合は、列挙を使用します。それ以外の場合、もう少し複雑なものが必要な場合、またはすべての一致が指定されているかどうかをコンパイラーに通知する場合は、ユースケースクラスを使用します。

26
Aaron

更新:以下のコードにはバグがあり、説明は here です。以下のテストプログラムは動作しますが、DayOfWeek自体の前に(たとえば)DayOfWeek.Monを使用すると、DayOfWeekが初期化されていないため失敗します(内部オブジェクトを使用しても外部オブジェクトは初期化されません)。メインクラスでval enums = Seq( DayOfWeek )などの操作を行って列挙型の初期化を強制する場合、またはchaotic3quilibriumの変更を使用する場合、このコードを使用できます。マクロベースの列挙型を楽しみにしています!


お望みならば

  • 完全ではないパターンマッチに関する警告
  • オプションで制御できる各列挙値に割り当てられたInt ID
  • 定義された順序での列挙値の不変のリスト
  • 名前から列挙値への不変のマップ
  • idからenum値への不変のマップ
  • すべてまたは特定の列挙値、または全体として列挙のメソッド/データを固定する場所
  • 順序付けられた列挙値(たとえば、日<水曜日かどうかをテストできます)
  • 1つの列挙型を拡張して他の列挙型を作成する機能

次に、以下が興味深い場合があります。フィードバックを歓迎します。

この実装には、抽象EnumおよびEnumVal基本クラスがあり、これらを拡張します。これらのクラスはすぐに表示されますが、最初に列挙型を定義する方法を次に示します。

object DayOfWeek extends Enum {
  sealed abstract class Val extends EnumVal
  case object Mon extends Val; Mon()
  case object Tue extends Val; Tue()
  case object Wed extends Val; Wed()
  case object Thu extends Val; Thu()
  case object Fri extends Val; Fri()
  case object Sat extends Val; Sat()
  case object Sun extends Val; Sun()
}

各列挙値を使用して(applyメソッドを呼び出す)、値を有効にする必要があることに注意してください。 [内部オブジェクトは、特にそうするように要求しない限り、遅延しないようにしたい。おもう。]

もちろん、必要に応じて、メソッド/データをDayOfWeek、Val、または個々のケースオブジェクトに追加することもできます。

そして、このような列挙型の使用方法は次のとおりです。

object DayOfWeekTest extends App {

  // To get a map from Int id to enum:
  println( DayOfWeek.valuesById )

  // To get a map from String name to enum:
  println( DayOfWeek.valuesByName )

  // To iterate through a list of the enum values in definition order,
  // which can be made different from ID order, and get their IDs and names:
  DayOfWeek.values foreach { v => println( v.id + " = " + v ) }

  // To sort by ID or name:
  println( DayOfWeek.values.sorted mkString ", " )
  println( DayOfWeek.values.sortBy(_.toString) mkString ", " )

  // To look up enum values by name:
  println( DayOfWeek("Tue") ) // Some[DayOfWeek.Val]
  println( DayOfWeek("Xyz") ) // None

  // To look up enum values by id:
  println( DayOfWeek(3) )         // Some[DayOfWeek.Val]
  println( DayOfWeek(9) )         // None

  import DayOfWeek._

  // To compare enums as ordinals:
  println( Tue < Fri )

  // Warnings about non-exhaustive pattern matches:
  def aufDeutsch( day: DayOfWeek.Val ) = day match {
    case Mon => "Montag"
    case Tue => "Dienstag"
    case Wed => "Mittwoch"
    case Thu => "Donnerstag"
    case Fri => "Freitag"
 // Commenting these out causes compiler warning: "match is not exhaustive!"
 // case Sat => "Samstag"
 // case Sun => "Sonntag"
  }

}

コンパイルすると次のようになります。

DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination            Sat
missing combination            Sun

  def aufDeutsch( day: DayOfWeek.Val ) = day match {
                                         ^
one warning found

このような警告が必要ない場合は、「日一致」を「(day:@unchecked)match」に置き換えるか、単にキャッチオールケースを最後に含めることができます。

上記のプログラムを実行すると、次の出力が得られます。

Map(0 -> Mon, 5 -> Sat, 1 -> Tue, 6 -> Sun, 2 -> Wed, 3 -> Thu, 4 -> Fri)
Map(Thu -> Thu, Sat -> Sat, Tue -> Tue, Sun -> Sun, Mon -> Mon, Wed -> Wed, Fri -> Fri)
0 = Mon
1 = Tue
2 = Wed
3 = Thu
4 = Fri
5 = Sat
6 = Sun
Mon, Tue, Wed, Thu, Fri, Sat, Sun
Fri, Mon, Sat, Sun, Thu, Tue, Wed
Some(Tue)
None
Some(Thu)
None
true

リストとマップは不変であるため、列挙型自体を壊すことなく、要素を簡単に削除してサブセットを作成できることに注意してください。

Enumクラス自体(およびその中のEnumVal)は次のとおりです。

abstract class Enum {

  type Val <: EnumVal

  protected var nextId: Int = 0

  private var values_       =       List[Val]()
  private var valuesById_   = Map[Int   ,Val]()
  private var valuesByName_ = Map[String,Val]()

  def values       = values_
  def valuesById   = valuesById_
  def valuesByName = valuesByName_

  def apply( id  : Int    ) = valuesById  .get(id  )  // Some|None
  def apply( name: String ) = valuesByName.get(name)  // Some|None

  // Base class for enum values; it registers the value with the Enum.
  protected abstract class EnumVal extends Ordered[Val] {
    val theVal = this.asInstanceOf[Val]  // only extend EnumVal to Val
    val id = nextId
    def bumpId { nextId += 1 }
    def compare( that:Val ) = this.id - that.id
    def apply() {
      if ( valuesById_.get(id) != None )
        throw new Exception( "cannot init " + this + " enum value twice" )
      bumpId
      values_ ++= List(theVal)
      valuesById_   += ( id       -> theVal )
      valuesByName_ += ( toString -> theVal )
    }
  }

}

そして、IDを制御し、データ/メソッドをVal抽象化とenum自体に追加する、より高度な使用法を次に示します。

object DayOfWeek extends Enum {

  sealed abstract class Val( val isWeekday:Boolean = true ) extends EnumVal {
    def isWeekend = !isWeekday
    val abbrev = toString take 3
  }
  case object    Monday extends Val;    Monday()
  case object   Tuesday extends Val;   Tuesday()
  case object Wednesday extends Val; Wednesday()
  case object  Thursday extends Val;  Thursday()
  case object    Friday extends Val;    Friday()
  nextId = -2
  case object  Saturday extends Val(false); Saturday()
  case object    Sunday extends Val(false);   Sunday()

  val (weekDays,weekendDays) = values partition (_.isWeekday)
}
15
AmigoNico

ここには、独自の値のリストを保持することなく、enum値として封印された特性/クラスを使用できるニースのシンプルなライブラリがあります。バグのあるknownDirectSubclassesに依存しない単純なマクロに依存しています。

https://github.com/lloydmeta/enumeratum

11
lloydmeta

2017年3月更新: Anthony Accioly がコメントしたとおり、scala.Enumeration/enum PRは終了しました。

Dotty(Scalaの次世代コンパイラ)がリードしますが、 dotty issue 197Martin OderskyのPR 1958


注:現在(2016年8月、6年以上後)scala.Enumerationを削除する提案があります: PR 5352

scala.Enumerationの廃止、@enumアノテーションの追加

構文

@enum
 class Toggle {
  ON
  OFF
 }

可能な実装例です。意図は、特定の制限(ネスト、再帰、またはコンストラクターパラメーターの変更なし)に準拠するADTもサポートすることです。 g .:

@enum
sealed trait Toggle
case object ON  extends Toggle
case object OFF extends Toggle

scala.Enumerationである軽減されない災害を非推奨にします。

Scena.Enumerationに対する@enumの利点:

  • 実際に動作します
  • Java相互運用
  • 消去の問題はありません
  • 列挙を定義するときに学ぶのに混乱するミニDSLはありません

欠点:なし。

これは、Scala-JVM、Scala.js、およびScala-Nativeをサポートするコードベースを1つ持つことができないという問題に対処します(JavaソースコードはScala.js/Scala-Nativeではサポートされず、ScalaソースコードはScala-JVMの既存のAPIで受け入れられる列挙型を定義できます)。

10
VonC

すべてのインスタンスで繰り返し処理またはフィルター処理する必要がある場合のケースクラスと列挙の別の欠点。これは、列挙の組み込み機能(およびJava列挙)ですが、ケースクラスはそのような機能を自動的にサポートしません。

つまり、「ケースクラスを使用して列挙値の合計セットのリストを取得する簡単な方法はありません」。

8
user142435

他のJVM言語(Javaなど)との相互運用性を維持することに真剣に取り組んでいる場合、最良のオプションはJava列挙型を記述することです。これらは、ScalaとJavaの両方のコードから透過的に機能します。これは、scala.Enumerationまたはcaseオブジェクトで言うことができる以上のものです。回避できる場合は、GitHubの新しい趣味プロジェクトごとに新しい列挙ライブラリを用意しないでください!

5
Connor Doyle

ケースクラスを列挙に似せるさまざまなバージョンを見てきました。これが私のバージョンです:

trait CaseEnumValue {
    def name:String
}

trait CaseEnum {
    type V <: CaseEnumValue
    def values:List[V]
    def unapply(name:String):Option[String] = {
        if (values.exists(_.name == name)) Some(name) else None
    }
    def unapply(value:V):String = {
        return value.name
    }
    def apply(name:String):Option[V] = {
        values.find(_.name == name)
    }
}

これにより、次のようなケースクラスを構築できます。

abstract class Currency(override name:String) extends CaseEnumValue {
}

object Currency extends CaseEnum {
    type V = Site
    case object EUR extends Currency("EUR")
    case object GBP extends Currency("GBP")
    var values = List(EUR, GBP)
}

たぶん、私がやったように各ケースクラスをリストに追加するよりも、誰かがより良いトリックを思いつくかもしれません。これが私が当時思いつくことができたすべてでした。

4
jho

私はそれらを必要とする最後の数回、これらの2つのオプションを行き来しています。最近まで、封印された特性/ケースオブジェクトオプションが私の好みでした。

1)Scala列挙宣言

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2)封印された特性+ケースオブジェクト

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

これらはどちらもJava列挙が提供するもののすべてを実際に満たしていませんが、賛否両論は以下のとおりです。

Scala列挙

長所:-オプションを使用してインスタンス化する関数、または正確であると直接仮定する関数(永続ストアからロードする場合に簡単)-すべての可能な値の反復がサポートされています

短所:-完全でない検索のコンパイル警告はサポートされていません(パターンマッチングが理想的ではなくなります)

ケースオブジェクト/封印された特性

長所:-封印された特性を使用して、一部の値を事前にインスタンス化できますが、作成時に他の値を注入できます-パターンマッチングの完全なサポート(定義されたメソッドの適用/適用解除)

短所:-永続ストアからのインスタンス化-ここでパターンマッチングを使用するか、すべての可能な「列挙値」の独自のリストを定義する必要があります。

最終的に私の意見を変えたのは、次のスニペットのようなものでした:

object DbInstrumentQueries {
  def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
    val symbol = rs.getString(tableAlias + ".name")
    val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
    val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
    val pointsValue = rs.getInt(tableAlias + ".points_value")
    val instrumentType = InstrumentType.fromString(rs.getString(tableAlias +".instrument_type"))
    val productType = ProductType.fromString(rs.getString(tableAlias + ".product_type"))

    Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
  }
}

object InstrumentType {
  def fromString(instrumentType: String): InstrumentType = Seq(CurrencyPair, Metal, CFD)
  .find(_.toString == instrumentType).get
}

object ProductType {

  def fromString(productType: String): ProductType = Seq(Commodity, Currency, Index)
  .find(_.toString == productType).get
}

.get呼び出しは恐ろしいものでした-代わりに列挙を使用すると、列挙のwithNameメソッドを次のように単純に呼び出すことができます。

object DbInstrumentQueries {
  def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
    val symbol = rs.getString(tableAlias + ".name")
    val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
    val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
    val pointsValue = rs.getInt(tableAlias + ".points_value")
    val instrumentType = InstrumentType.withNameString(rs.getString(tableAlias + ".instrument_type"))
    val productType = ProductType.withName(rs.getString(tableAlias + ".product_type"))

    Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
  }
}

ですから、今後は、リポジトリから値にアクセスする場合は列挙型を使用し、それ以外の場合はオブジェクト/シールされた特性を使用することが私の好みだと思います。

2
Mad Dog

私はcase objectsを好みます(個人的な好みの問題です)。そのアプローチに固有の問題(文字列を解析し、すべての要素を反復処理する)に対処するために、完璧ではないが効果的な行をいくつか追加しました。

ここにコードを貼り付けて、それが有用であり、他の人がそれを改善できることを期待しています。

/**
 * Enum for Genre. It contains the type, objects, elements set and parse method.
 *
 * This approach supports:
 *
 * - Pattern matching
 * - Parse from name
 * - Get all elements
 */
object Genre {
  sealed trait Genre

  case object MALE extends Genre
  case object FEMALE extends Genre

  val elements = Set (MALE, FEMALE) // You have to take care this set matches all objects

  def apply (code: String) =
    if (MALE.toString == code) MALE
    else if (FEMALE.toString == code) FEMALE
    else throw new IllegalArgumentException
}

/**
 * Enum usage (and tests).
 */
object GenreTest extends App {
  import Genre._

  val m1 = MALE
  val m2 = Genre ("MALE")

  assert (m1 == m2)
  assert (m1.toString == "MALE")

  val f1 = FEMALE
  val f2 = Genre ("FEMALE")

  assert (f1 == f2)
  assert (f1.toString == "FEMALE")

  try {
    Genre (null)
    assert (false)
  }
  catch {
    case e: IllegalArgumentException => assert (true)
  }

  try {
    Genre ("male")
    assert (false)
  }
  catch {
    case e: IllegalArgumentException => assert (true)
  }

  Genre.elements.foreach { println }
}
2
jamming

case classesenumerationsよりも優れている最大の利点は、type class patternakaad-hoc polymorphysm。次のような列挙型を一致させる必要はありません。

someEnum match {
  ENUMA => makeThis()
  ENUMB => makeThat()
}

代わりに次のようなものがあります:

def someCode[SomeCaseClass](implicit val maker: Maker[SomeCaseClass]){
  maker.make()
}

implicit val makerA = new Maker[CaseClassA]{
  def make() = ...
}
implicit val makerB = new Maker[CaseClassB]{
  def make() = ...
}
0
Murat Mustafin

GatesDaの仕事に対する答え を取得する方法をまだ探している人のために:それをインスタンス化するために宣言した後にケースオブジェクトを参照することができます:

trait Enum[A] {
  trait Value { self: A =>
    _values :+= this
  }
  private var _values = List.empty[A]
  def values = _values
}

sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
  case object EUR extends Currency; 
  EUR //THIS IS ONLY CHANGE
  case object GBP extends Currency; GBP //Inline looks better
}
0
V-Lamp