タイプセーフな列挙型をモデル化する方法は?


311

Scalaにはenum、Javaのようにタイプセーフなはありません。関連する定数のセットが与えられた場合、Scalaでそれらの定数を表すための最良の方法は何でしょうか?


2
なぜjava enumを使用しないのですか?これは、私がまだプレーンJavaを使用することを好む数少ないものの1つです。
最大の

1
私はscalaの列挙と代替について簡単な概要を書きましたが、それが役に立つかもしれません:pedrorijo.com/blog/scala-enums/
pedrorijo91

回答:


187

http://www.scala-lang.org/docu/files/api/scala/Enumeration.html

使用例

  object Main extends App {

    object WeekDay extends Enumeration {
      type WeekDay = Value
      val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
    }
    import WeekDay._

    def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

    WeekDay.values filter isWorkingDay foreach println
  }

2
真剣に、アプリケーションは使用されるべきではありません。修正されませんでした。新しいクラスAppが導入されましたが、Schildmeijerが言及した問題はありません。「object foo extends App {...}」を実行すると、args変数を介してコマンドライン引数にすぐにアクセスできます。
AmigoNico

scala.Enumeration(上記の「オブジェクトWeekDay」コードサンプルで使用しているもの)は、完全なパターンマッチングを提供していません。Scalaで現在使用されているすべての列挙型パターンを調査し、それらの概要をこのStackOverflowの回答で説明しました(scala.Enumerationと「封印された特性+ケースオブジェクト」の両方のパターンの最良のパターンを提供する新しいパターン:stackoverflow。 com / a / 25923651/501113
chaotic3quilibrium

377

上記のskaffmanによってScalaのドキュメントからコピーされた例は、実際には限られた有用性しかありません(sを使用することもできます)。case object

Javaのに似ている最も密接に何かを得るためにはEnum(賢明にすなわちtoStringvalueOf-おそらくあなたは、データベースに列挙値を永続化されている方法)あなたはそれを少し変更する必要があります。skaffmanのコードを使用した場合:

WeekDay.valueOf("Sun") //returns None
WeekDay.Tue.toString   //returns Weekday(2)

一方、次の宣言を使用します。

object WeekDay extends Enumeration {
  type WeekDay = Value
  val Mon = Value("Mon")
  val Tue = Value("Tue") 
  ... etc
}

より実用的な結果が得られます。

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue

7
ところで valueOfメソッドが
無効に

36
@macias valueOfの置換はwithNameであり、これはオプションを返さず、一致しない場合はNSEをスローします。なんと!
Bluu 2012年

6
@Bluu自分​​でvalueOfを追加できます:def valueOf(name:String)= WeekDay.values.find(_。toString == name)オプションを
追加

@centrを作成しMap[Weekday.Weekday, Long]て値を追加しようとMonすると、コンパイラは無効なタイプのエラーをスローします。期待される平日。平日は値を見つけましたか?なぜこれが起こるのですか?
Sohaib 2015年

@Sohaib Map [Weekday.Value、Long]である必要があります。
2015年

99

やり方はたくさんあります。

1)記号を使用します。ただし、シンボルが期待される場所で非シンボルを受け入れないことは別として、タイプセーフは提供されません。ここでは、完全を期すためだけに言及しています。次に使用例を示します。

def update(what: Symbol, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case 'row => replaceRow(where, newValue)
    case 'col | 'column => replaceCol(where, newValue)
    case _ => throw new IllegalArgumentException
  }

// At REPL:   
scala> val a = unitMatrixInt(3)
a: teste7.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a('row, 1) = a.row(0)
res41: teste7.MatrixInt =
/ 1 0 0 \
| 1 0 0 |
\ 0 0 1 /

scala> a('column, 2) = a.row(0)
res42: teste7.MatrixInt =
/ 1 0 1 \
| 0 1 0 |
\ 0 0 0 /

2)クラスの使用Enumeration

object Dimension extends Enumeration {
  type Dimension = Value
  val Row, Column = Value
}

または、シリアル化または表示する必要がある場合:

object Dimension extends Enumeration("Row", "Column") {
  type Dimension = Value
  val Row, Column = Value
}

これは次のように使用できます。

def update(what: Dimension, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case Row => replaceRow(where, newValue)
    case Column => replaceCol(where, newValue)
  }

// At REPL:
scala> a(Row, 2) = a.row(1)
<console>:13: error: not found: value Row
       a(Row, 2) = a.row(1)
         ^

scala> a(Dimension.Row, 2) = a.row(1)
res1: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

scala> import Dimension._
import Dimension._

scala> a(Row, 2) = a.row(1)
res2: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

残念ながら、すべての一致が考慮されるとは限りません。行または列を一致に含めるのを忘れた場合、Scalaコンパイラーは警告を表示しませんでした。だからそれは私にいくつかを与えます型安全性が、得ることができるほどではありません。

3)ケースオブジェクト:

sealed abstract class Dimension
case object Row extends Dimension
case object Column extends Dimension

ここで、のケースを省略した場合match、コンパイラーは警告を出します。

MatrixInt.scala:70: warning: match is not exhaustive!
missing combination         Column

    what match {
    ^
one warning found

それはほとんど同じ方法で使用され、必要さえありませんimport

scala> val a = unitMatrixInt(3)
a: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a(Row,2) = a.row(0)
res15: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 1 0 0 /

では、なぜケースオブジェクトの代わりに列挙型を使用するのか疑問に思うかもしれません。実際、ここのように、ケースオブジェクトには多くの利点があります。ただし、Enumerationクラスには、要素(Scala 2.8のイテレーター)などの多くのコレクションメソッドがあり、イテレーター、マップ、フラットマップ、フィルターなどを返します。

この回答は基本的に、私のブログのこの記事から抜粋した部分です。


「...記号が必要な場所で非記号を受け入れない」> Symbolインスタンスにスペースや特殊文字を含めることはできないと思います。ほとんどの人は、Symbolクラスに初めて出会ったとき、おそらくそう思っていますが、実際には正しくありません。Symbol("foo !% bar -* baz")コンパイルして完全に正常に実行します。つまり、任意の文字列をSymbolラップするインスタンスを完全に作成できます(「単一コマ」の構文糖でそれを実行することはできません)。保証される唯一のことは、与えられたシンボルの一意性であり、比較と照合がわずかに速くなります。Symbol
レジスジャンジル

@RégisJean-Gillesいいえ、Stringたとえば、引数としてをSymbolパラメーターに渡すことはできません。
ダニエルC.ソブラル

はい、その部分は理解しましStringたが、基本的に文字列のラッパーであり、双方向に自由に変換できる別のクラスに置き換える場合、それはかなり重要なポイントです(の場合のようにSymbol)。それが「型の安全性を与えない」と言ったときのことだと思いますが、OPが型安全なソリューションを明示的に求めたので、それはあまり明確ではありませんでした。あなたを書いている時点で、それらがすべてでは列挙型ではないので、それは安全で入力していないが、されていないだけのことを知っていたかどうかわかりませんでしたまた、 Symbol sが渡された引数が特殊な文字を持たないということでも保証はしません。
レジスジャンジル

1
詳述すると、「シンボルが期待される場所で非シンボルを受け入れない」と言う場合、それは「シンボルのインスタンスではない値を受け入れない」(これは明らかに真実です)または「受け入れられない値を受け入れないプレーンな識別子のような文字列、別名「記号」は、」(事実ではない、との最初の出会いは、特殊なのにであることに起因し、ほとんど誰もが、我々はScalaのシンボルに遭遇した最初の時間を持っていることを誤解している'foo表記排除してし非識別子文字列)。これは私が将来の読者のために払拭したかったこの誤解です。
レジスジャンジル

@RégisJean-Gilles前者のことで、明らかに真実です。つまり、静的型付けに慣れている人なら誰でもそうです。当時、静的型付けと「動的」型付けの相対的なメリットについて多くの議論があり、Scalaに関心のある人の多くは動的型付けのバックグラウンドを持っていたので、言うまでもありませんでした。今日はそのような発言をすることすら考えていません。個人的には、Scalaのシンボルは醜く冗長であり、決して使用しないと思います。最後のコメントは良い点なので賛成です。
ダニエルC.ソブラル

52

名前付き列挙を宣言する少し冗長ではない方法:

object WeekDay extends Enumeration("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") {
  type WeekDay = Value
  val Sun, Mon, Tue, Wed, Thu, Fri, Sat = Value
}

WeekDay.valueOf("Wed") // returns Some(Wed)
WeekDay.Fri.toString   // returns Fri

もちろん、ここでの問題は、名前とvalの順序を同期させておく必要があることです。これは、nameとvalが同じ行で宣言されている場合に簡単です。


11
これは一見すっきりしているように見えますが、メンテナが両方のリストの順序を同期させる必要があるという欠点があります。曜日の例では、それはありそうにありません。しかし、一般に、新しい値が挿入されるか、1つが削除され、2つのリストが同期しなくなる可能性があります。その場合、微妙なバグが発生する可能性があります。
ブレントファウスト

1
以前のコメントによると、リスクは2つの異なるリストが暗黙のうちに同期されなくなる可能性があることです。これは現在の小さな例では問題ではありませんが、メンバーがさらに多い場合(数十から数百など)、2つのリストが静かに同期しなくなる確率は大幅に高くなります。また、scala.Enumerationは、Scalaのコンパイル時の完全なパターンマッチングの警告/エラーから利益を得ることができません。2つのリストが同期されていることを確認するためにランタイムチェックを実行するソリューションを含むStackOverflow回答を作成しました:stackoverflow.com/a/25923651/501113
chaotic3quilibrium

17

次のように、列挙の代わりにシールされた抽象クラスを使用できます。

sealed abstract class Constraint(val name: String, val verifier: Int => Boolean)

case object NotTooBig extends Constraint("NotTooBig", (_ < 1000))
case object NonZero extends Constraint("NonZero", (_ != 0))
case class NotEquals(x: Int) extends Constraint("NotEquals " + x, (_ != x))

object Main {

  def eval(ctrs: Seq[Constraint])(x: Int): Boolean =
    (true /: ctrs){ case (accum, ctr) => accum && ctr.verifier(x) }

  def main(args: Array[String]) {
    val ctrs = NotTooBig :: NotEquals(5) :: Nil
    val evaluate = eval(ctrs) _

    println(evaluate(3000))
    println(evaluate(3))
    println(evaluate(5))
  }

}

ケースオブジェクトを持つ封印された特性も可能です。
Ashalynd 2013年

2
「封印された特性+ケースオブジェクト」パターンには、StackOverflowの回答で詳しく説明する問題があります。しかし、私はまた、スレッドで覆われ、このパターンに関連するすべての問題を解決する方法を見つけ出すをした:stackoverflow.com/a/25923651/501113
chaotic3quilibrium


2

Scalaの「列挙型」に関するすべてのオプションについて広範な調査を行った後、このドメインのより完全な概要を別のStackOverflowスレッドに投稿しました。これには、JVMクラス/オブジェクトの初期化順序の問題を解決した「封印された特性+ケースオブジェクト」パターンの解決策が含まれています。



1

Scalaでは、https://github.com/lloydmeta/enumeratumで非常に快適です

プロジェクトは例とドキュメントで本当に良いです

彼らのドキュメントからのこの例だけであなたは興味を持つはずです

import enumeratum._

sealed trait Greeting extends EnumEntry

object Greeting extends Enum[Greeting] {

  /*
   `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`

   You use it to implement the `val values` member
  */
  val values = findValues

  case object Hello   extends Greeting
  case object GoodBye extends Greeting
  case object Hi      extends Greeting
  case object Bye     extends Greeting

}

// Object Greeting has a `withName(name: String)` method
Greeting.withName("Hello")
// => res0: Greeting = Hello

Greeting.withName("Haro")
// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)

// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]
Greeting.withNameOption("Hello")
// => res1: Option[Greeting] = Some(Hello)

Greeting.withNameOption("Haro")
// => res2: Option[Greeting] = None

// It is also possible to use strings case insensitively
Greeting.withNameInsensitive("HeLLo")
// => res3: Greeting = Hello

Greeting.withNameInsensitiveOption("HeLLo")
// => res4: Option[Greeting] = Some(Hello)

// Uppercase-only strings may also be used
Greeting.withNameUppercaseOnly("HELLO")
// => res5: Greeting = Hello

Greeting.withNameUppercaseOnlyOption("HeLLo")
// => res6: Option[Greeting] = None

// Similarly, lowercase-only strings may also be used
Greeting.withNameLowercaseOnly("hello")
// => res7: Greeting = Hello

Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.