Wie modelliere ich typsichere Aufzählungstypen?

311

Scala hat keine typsicheren enums wie Java. Was wäre angesichts einer Reihe verwandter Konstanten die beste Möglichkeit in Scala, diese Konstanten darzustellen?

Jesper
quelle
2
Warum nicht einfach Java Enum verwenden? Dies ist eines der wenigen Dinge, die ich immer noch lieber mit einfachem Java verwende.
Max
1
Ich habe einen kleinen Überblick über die Aufzählung und Alternativen von Scala geschrieben. Vielleicht finden Sie ihn nützlich: pedrorijo.com/blog/scala-enums/
pedrorijo91

Antworten:

187

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

Beispiel Verwendung

  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
  }
Skaffman
quelle
2
Im Ernst, Anwendung sollte nicht verwendet werden. Es wurde NICHT behoben; Eine neue Klasse, App, wurde eingeführt, die nicht die von Schildmeijer erwähnten Probleme hat. Also "object foo erweitert App {...}" Und Sie haben über die Variable args sofortigen Zugriff auf Befehlszeilenargumente.
AmigoNico
scala.Enumeration (das ist, was Sie in Ihrem obigen Codebeispiel "object WeekDay" verwenden) bietet keinen erschöpfenden Mustervergleich. Ich habe alle verschiedenen Aufzählungsmuster untersucht, die derzeit in Scala verwendet werden, und in dieser StackOverflow-Antwort einen Überblick darüber gegeben (einschließlich eines neuen Musters, das das Beste aus beiden scala.Enumeration- und dem Muster "Sealed Trait + Case Object" bietet
chaotic3quilibrium
377

Ich muss sagen , dass das Beispiel der Scala Dokumentation herauskopiert durch skaffman oben in der Praxis ist von begrenztem Nutzen (Sie könnte genauso gut verwenden case objects).

Um etwas zu erhalten, das einem Java am ähnlichsten ist Enum(dh mit Sinn toStringund valueOfMethoden - vielleicht behalten Sie die Enum-Werte in einer Datenbank bei), müssen Sie es ein wenig ändern. Wenn Sie den Skaffman -Code verwendet haben:

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

Unter Verwendung der folgenden Erklärung:

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

Sie erhalten vernünftigere Ergebnisse:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
quelle
7
Übrigens. Die Methode valueOf ist jetzt tot :-(
greenoldman
36
@macias valueOf'Ersatz ist withName, der keine Option zurückgibt und einen NSE auslöst , wenn keine Übereinstimmung vorliegt . Was zum!
Bluu
6
@Bluu Sie können valueOf selbst hinzufügen: def valueOf (name: String) = WeekDay.values.find (_. ToString == name), um eine Option zu haben
zentriert am
@centr Wenn ich versuche, Map[Weekday.Weekday, Long]einen Wert zu erstellen und einen Wert hinzuzufügen Mon, gibt der Compiler einen ungültigen Typfehler aus . Erwarteter Wochentag. Wochentag gefunden Wert? Warum passiert das?
Sohaib
@ Sohaib Es sollte Map [Weekday.Value, Long] sein.
Mitte
99

Es gibt viele Möglichkeiten.

1) Verwenden Sie Symbole. Es gibt Ihnen jedoch keine Typensicherheit, abgesehen davon, dass Sie keine Nicht-Symbole akzeptieren, bei denen ein Symbol erwartet wird. Ich erwähne es hier nur der Vollständigkeit halber. Hier ist ein Anwendungsbeispiel:

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) Klasse verwenden Enumeration:

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

oder, wenn Sie es serialisieren oder anzeigen müssen:

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

Dies kann folgendermaßen verwendet werden:

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 /

Leider wird nicht sichergestellt, dass alle Übereinstimmungen berücksichtigt werden. Wenn ich vergessen hätte, Zeile oder Spalte in das Match aufzunehmen, hätte mich der Scala-Compiler nicht gewarnt. Es gibt mir also eine gewisse Sicherheit, aber nicht so viel, wie man gewinnen kann.

3) Fallobjekte:

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

Wenn ich jetzt einen Fall auf a matchauslasse, warnt mich der Compiler:

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

    what match {
    ^
one warning found

Es wird fast genauso verwendet und benötigt nicht einmal ein 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 /

Sie fragen sich vielleicht, warum Sie jemals eine Aufzählung anstelle von Fallobjekten verwenden sollten. In der Tat haben Fallobjekte viele Vorteile, wie hier. Die Enumeration-Klasse verfügt jedoch über viele Collection-Methoden, z. B. Elemente (Iterator in Scala 2.8), die einen Iterator, eine Map, eine FlatMap, einen Filter usw. zurückgeben.

Diese Antwort ist im Wesentlichen ein ausgewählter Teil dieses Artikels in meinem Blog.

Daniel C. Sobral
quelle
"... Nicht-Symbole nicht akzeptieren, wenn ein Symbol erwartet wird"> Ich vermute, Sie meinen, dass SymbolInstanzen keine Leerzeichen oder Sonderzeichen haben dürfen. Die meisten Leute, die die SymbolKlasse zum ersten Mal treffen, denken das wahrscheinlich, sind aber tatsächlich falsch. Symbol("foo !% bar -* baz")kompiliert und läuft einwandfrei. Mit anderen Worten, Sie können perfekt SymbolInstanzen erstellen, die eine beliebige Zeichenfolge umschließen (Sie können dies einfach nicht mit dem syntaktischen Zucker "Einzelkoma" tun). Das einzige, was Symbolgarantiert, ist die Einzigartigkeit eines bestimmten Symbols, wodurch es geringfügig schneller verglichen und abgeglichen werden kann.
Régis Jean-Gilles
@ RégisJean-Gilles Nein, ich meine, Sie können beispielsweise kein a Stringals Argument an einen SymbolParameter übergeben.
Daniel C. Sobral
Ja, ich habe diesen Teil verstanden, aber es ist ein ziemlich strittiger Punkt, wenn Sie ihn durch eine Stringandere Klasse ersetzen , die im Grunde ein Wrapper um einen String ist und frei in beide Richtungen konvertiert werden kann (wie es der Fall ist Symbol). Ich denke, das haben Sie gemeint, als Sie sagten "Es gibt Ihnen keine Typensicherheit". Es war einfach nicht sehr klar, da OP ausdrücklich nach typsicheren Lösungen gefragt hat. Ich war mir nicht sicher, ob Sie zum Zeitpunkt des Schreibens wussten, dass es nicht nur nicht typsicher ist, da es sich überhaupt nicht um Aufzählungen handelt, sondern auch Symbol nicht einmal garantiert, dass das übergebene Argument keine Sonderzeichen enthält.
Régis Jean-Gilles
1
Wenn Sie sagen "Nicht-Symbole nicht akzeptieren, wenn ein Symbol erwartet wird", kann dies entweder als "Nicht akzeptieren von Werten, die keine Instanzen von Symbolen sind" (was offensichtlich wahr ist) oder "Nicht akzeptieren von Werten, die keine Symbole sind" gelesen werden einfache identifikatorähnliche Zeichenfolgen, auch bekannt als "Symbole" (was nicht wahr ist und ein Missverständnis ist, dass so ziemlich jeder das erste Mal auf Scala-Symbole stößt, da die erste Begegnung jedoch die spezielle 'fooNotation ist, die dies ausschließt Nicht-Bezeichner-Zeichenfolgen). Dies ist dieses Missverständnis, das ich für jeden zukünftigen Leser zerstreuen wollte.
Régis Jean-Gilles
@ RégisJean-Gilles Ich meinte das erstere, das offensichtlich wahr ist. Ich meine, es ist offensichtlich für jeden wahr, der an statisches Tippen gewöhnt ist. Damals gab es viele Diskussionen über die relativen Vorzüge der statischen und "dynamischen" Typisierung, und viele Leute, die sich für Scala interessierten, kamen aus einem dynamischen Typisierungshintergrund, daher dachte ich, dass dies nicht selbstverständlich war. Ich würde heutzutage nicht einmal daran denken, diese Bemerkung zu machen. Persönlich denke ich, dass Scalas Symbol hässlich und überflüssig ist und es niemals benutzt. Ich stimme Ihrem letzten Kommentar zu, da dies ein guter Punkt ist.
Daniel C. Sobral
52

Eine etwas weniger ausführliche Art, benannte Aufzählungen zu deklarieren:

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

Das Problem hierbei ist natürlich, dass Sie die Reihenfolge der Namen und Werte synchron halten müssen, was einfacher ist, wenn Name und Wert in derselben Zeile deklariert sind.

Walter Chang
quelle
11
Dies sieht auf den ersten Blick sauberer aus, hat jedoch den Nachteil, dass der Betreuer die Reihenfolge beider Listen synchron halten muss. Für das Beispiel der Wochentage erscheint dies nicht wahrscheinlich. Im Allgemeinen kann jedoch ein neuer Wert eingefügt oder einer gelöscht werden, und die beiden Listen können nicht synchron sein. In diesem Fall können subtile Fehler auftreten.
Brent Faust
1
Laut vorherigem Kommentar besteht das Risiko, dass die beiden unterschiedlichen Listen stillschweigend nicht mehr synchron sind. Während es für Ihr aktuelles kleines Beispiel kein Problem ist, ist die Wahrscheinlichkeit, dass die beiden Listen stillschweigend nicht mehr synchron sind, wesentlich höher, wenn es viel mehr Mitglieder gibt (wie Dutzende bis Hunderte). Auch scala.Enumeration kann nicht von Scalas Kompilierungszeit profitieren. Ich habe eine StackOverflow-Antwort erstellt, die eine Lösung enthält, die eine Laufzeitprüfung durchführt, um sicherzustellen, dass die beiden Listen synchron bleiben: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Sie können anstelle der Aufzählung eine versiegelte abstrakte Klasse verwenden, zum Beispiel:

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))
  }

}
Ron
quelle
Eine versiegelte Eigenschaft mit Fallobjekten ist ebenfalls möglich.
Ashalynd
2
Das Muster "Versiegelte Merkmale + Fallobjekte" weist Probleme auf, die ich in einer StackOverflow-Antwort detailliert darstelle. Ich habe jedoch herausgefunden, wie alle Probleme im Zusammenhang mit diesem Muster behoben werden können
chaotic3quilibrium
7

habe gerade Enumeratum entdeckt . Es ist ziemlich erstaunlich und ebenso erstaunlich, dass es nicht bekannter ist!

Praktisch
quelle
2

Nachdem ich mich in Scala eingehend mit allen Optionen rund um "Aufzählungen" befasst hatte, veröffentlichte ich einen viel vollständigeren Überblick über diese Domain in einem anderen StackOverflow-Thread . Es enthält eine Lösung für das Muster "Sealed Trait + Case Object", bei dem ich das Problem mit der Reihenfolge der JVM-Klassen- / Objektinitialisierung gelöst habe.

chaotisches Gleichgewicht
quelle
1

Dotty (Scala 3) wird native Enums unterstützen. Überprüfen Sie hier und hier .

Nullon
quelle
1

In Scala ist es sehr bequem mit https://github.com/lloydmeta/enumeratum

Das Projekt ist wirklich gut mit Beispielen und Dokumentation

Nur dieses Beispiel aus ihren Dokumenten sollte Sie interessieren

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)
Dmitriy Kuzkin
quelle