Fallobjekte gegen Aufzählungen in Scala

231

Gibt es Best-Practice-Richtlinien für die Verwendung von Fallklassen (oder Fallobjekten) im Vergleich zur Erweiterung der Aufzählung in Scala?

Sie scheinen einige der gleichen Vorteile zu bieten.

Alex Miller
quelle
2
Ich habe einen kleinen Überblick über Scala-Aufzählung und Alternativen geschrieben. Vielleicht finden Sie ihn nützlich: pedrorijo.com/blog/scala-enums/
pedrorijo91
1
Siehe auch die Dotty-basierte Scala 3enum (für Mitte 2020).
VonC

Antworten:

223

Ein großer Unterschied besteht darin, dass Enumerationdie Instanziierung von einem nameString unterstützt wird. Beispielsweise:

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

Dann können Sie tun:

val ccy = Currency.withName("EUR")

Dies ist nützlich, wenn Sie Aufzählungen (z. B. in einer Datenbank) beibehalten oder aus Daten erstellen möchten, die sich in Dateien befinden. Im Allgemeinen finde ich jedoch, dass Aufzählungen in Scala etwas ungeschickt sind und das Gefühl eines umständlichen Add-Ons haben, weshalb ich jetzt eher case objects verwende. A case objectist flexibler als eine Aufzählung:

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

case class UnknownCurrency(name: String) extends Currency

Jetzt habe ich den Vorteil ...

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

Wie @ chaotic3quilibrium hervorhob (mit einigen Korrekturen, um das Lesen zu erleichtern):

In Bezug auf das Muster "UnknownCurrency (Code)" gibt es andere Möglichkeiten, um keine Währungscode-Zeichenfolge zu finden, als die geschlossene Menge des CurrencyTyps zu "brechen" . UnknownCurrencyWenn Sie vom Typ sind, Currencykönnen Sie sich jetzt in andere Teile einer API einschleichen.

Es ist ratsam, diesen Fall nach außen zu verschieben Enumerationund den Kunden dazu zu bringen, sich mit einem Option[Currency]Typ zu befassen , der eindeutig anzeigt, dass tatsächlich ein Übereinstimmungsproblem vorliegt, und den Benutzer der API zu "ermutigen", es selbst zu klären.

Um die anderen Antworten hier weiterzuverfolgen, sind die Hauptnachteile von case objects über Enumerations:

  1. Kann nicht über alle Instanzen der "Aufzählung" iterieren . Dies ist sicherlich der Fall, aber ich habe es in der Praxis als äußerst selten empfunden, dass dies erforderlich ist.

  2. Kann nicht einfach aus dem dauerhaften Wert instanziieren . Dies gilt auch, aber außer bei großen Aufzählungen (z. B. allen Währungen) bedeutet dies keinen großen Aufwand.

oxbow_lakes
quelle
10
Der andere Unterschied besteht darin, dass die Aufzählung enum sofort bestellt wird, während die auf Fallobjekten basierende Aufzählung offensichtlich nicht
om-nom-nom
1
Ein weiterer Punkt für Fallobjekte ist, wenn Sie sich für die Java-Interoperabilität interessieren. Die Aufzählung würde die Werte als Aufzählung zurückgeben. Wert, also 1) eine Scala-Bibliothek erforderlich, 2) die tatsächlichen Typinformationen verlieren.
Juanmirocks
7
@oxbow_lakes In Bezug auf Punkt 1, speziell diesen Teil "... ich habe es in der Praxis als äußerst selten empfunden, dass dies erforderlich ist": Anscheinend erledigen Sie selten viel UI-Arbeit. Dies ist ein äußerst häufiger Anwendungsfall. Anzeigen einer (Dropdown-) Liste gültiger Aufzählungselemente, aus denen Sie auswählen können.
chaotisches Gleichgewicht
Ich verstehe den Typ des Gegenstands, der trade.ccyim Beispiel des versiegelten Merkmals übereinstimmt, nicht .
Rloth
und case objecterzeugen Sie keinen größeren (~ 4x) Code-Footprint als Enumeration? Nützliche Unterscheidung insbesondere für scala.jsProjekte mit geringem Platzbedarf.
Öko
69

UPDATE: Es wurde eine neue makrobasierte Lösung erstellt, die der unten beschriebenen Lösung weit überlegen ist. Ich empfehle dringend, diese neue makrobasierte Lösung zu verwenden . Und es scheint, dass Pläne für Dotty diesen Stil der Enum-Lösung zu einem Teil der Sprache machen werden. Whoohoo!

Zusammenfassung:
Es gibt drei grundlegende Muster für den Versuch, Java Enumin einem Scala-Projekt zu reproduzieren . Zwei der drei Muster; direkt mit Java Enumund scala.Enumerationkönnen den umfassenden Mustervergleich von Scala nicht aktivieren. Und der dritte; "Sealed Trait + Case Object", tut dies ... hat jedoch Komplikationen bei der Initialisierung von JVM-Klassen / Objekten, die zu einer inkonsistenten Generierung des Ordnungsindex führen.

Ich habe eine Lösung mit zwei Klassen erstellt. Enumeration und EnumerationDecorated befinden sich in dieser Übersicht . Ich habe den Code nicht in diesen Thread gepostet, da die Datei für die Aufzählung ziemlich groß war (+400 Zeilen - enthält viele Kommentare, die den Implementierungskontext erklären).

Details:
Die Frage, die Sie stellen, ist ziemlich allgemein; "... wann man caseKlassen benutzt oderobjects erweitert [scala.]Enumeration". Und es stellt sich heraus, dass es VIELE mögliche Antworten gibt, wobei jede Antwort von den Feinheiten der spezifischen Projektanforderungen abhängt, die Sie haben. Die Antwort kann auf drei Grundmuster reduziert werden.

Stellen wir zunächst sicher, dass wir von der gleichen Grundidee einer Aufzählung ausgehen. Definieren wir eine Aufzählung hauptsächlich anhand der Enumab Java 5 (1.5) bereitgestellten :

  1. Es enthält eine natürlich geordnete geschlossene Gruppe benannter Mitglieder
    1. Es gibt eine feste Anzahl von Mitgliedern
    2. Mitglieder sind natürlich geordnet und explizit indiziert
      • Im Gegensatz zur Sortierung nach einigen abfragbaren Kriterien für Mitglieder
    3. Jedes Mitglied hat einen eindeutigen Namen in der Gesamtmenge aller Mitglieder
  2. Alle Mitglieder können basierend auf ihren Indizes leicht durchlaufen werden
  3. Ein Mitglied kann mit seinem Namen (Groß- und Kleinschreibung beachten) abgerufen werden
    1. Es wäre sehr schön, wenn ein Mitglied auch mit seinem Namen ohne Berücksichtigung der Groß- und Kleinschreibung abgerufen werden könnte
  4. Ein Mitglied kann mit seinem Index abgerufen werden
  5. Mitglieder können die Serialisierung einfach, transparent und effizient verwenden
  6. Mitglieder können leicht erweitert werden, um zusätzliche zugehörige Singleton-Daten zu speichern
  7. Wenn Sie über Javas hinausdenken Enum, wäre es schön, Scalas Musterübereinstimmungsprüfung für eine Aufzählung explizit nutzen zu können

Schauen wir uns als nächstes die zusammengefassten Versionen der drei am häufigsten veröffentlichten Lösungsmuster an:

A) Tatsächlich direkt mit Java-Enum Muster (in einem gemischten Scala / Java-Projekt):

public enum ChessPiece {
    KING('K', 0)
  , QUEEN('Q', 9)
  , BISHOP('B', 3)
  , KNIGHT('N', 3)
  , ROOK('R', 5)
  , PAWN('P', 1)
  ;

  private char character;
  private int pointValue;

  private ChessPiece(char character, int pointValue) {
    this.character = character; 
    this.pointValue = pointValue;   
  }

  public int getCharacter() {
    return character;
  }

  public int getPointValue() {
    return pointValue;
  }
}

Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar:

  1. 3.1 - Es wäre sehr schön, wenn ein Mitglied auch mit seinem Namen ohne Berücksichtigung der Groß- und Kleinschreibung abgerufen werden könnte
  2. 7 - Wenn Sie über Javas Enum hinausdenken, wäre es schön, Scalas Musterübereinstimmungsprüfung für eine Aufzählung explizit nutzen zu können

Bei meinen aktuellen Projekten habe ich nicht den Vorteil, die Risiken rund um den gemischten Scala / Java-Projektpfad einzugehen. Und selbst wenn ich mich für ein gemischtes Projekt entscheiden könnte, ist Punkt 7 entscheidend, damit ich Probleme mit der Kompilierungszeit erkennen kann, wenn ich Aufzählungselemente hinzufüge / entferne oder neuen Code schreibe, um mit vorhandenen Aufzählungsmitgliedern umzugehen.


B) Verwenden des " sealed trait+case objects " - Musters:

sealed trait ChessPiece {def character: Char; def pointValue: Int}
object ChessPiece {
  case object KING extends ChessPiece {val character = 'K'; val pointValue = 0}
  case object QUEEN extends ChessPiece {val character = 'Q'; val pointValue = 9}
  case object BISHOP extends ChessPiece {val character = 'B'; val pointValue = 3}
  case object KNIGHT extends ChessPiece {val character = 'N'; val pointValue = 3}
  case object ROOK extends ChessPiece {val character = 'R'; val pointValue = 5}
  case object PAWN extends ChessPiece {val character = 'P'; val pointValue = 1}
}

Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar:

  1. 1.2 - Mitglieder sind natürlich geordnet und explizit indexiert
  2. 2 - Alle Mitglieder können anhand ihrer Indizes leicht durchlaufen werden
  3. 3 - Ein Mitglied kann mit seinem Namen (Groß- und Kleinschreibung beachten) abgerufen werden
  4. 3.1 - Es wäre sehr schön, wenn ein Mitglied auch mit seinem Namen ohne Berücksichtigung der Groß- und Kleinschreibung abgerufen werden könnte
  5. 4 - Ein Mitglied kann mit seinem Index abgerufen werden

Es ist fraglich, ob es die Aufzählungsdefinitionspunkte 5 und 6 wirklich erfüllt. Für 5 ist es eine Strecke zu behaupten, dass es effizient ist. Für 6 ist es nicht einfach, zusätzliche zugehörige Singleton-Daten zu speichern.


C) Verwenden des scala.EnumerationMusters (inspiriert von dieser StackOverflow-Antwort ):

object ChessPiece extends Enumeration {
  val KING = ChessPieceVal('K', 0)
  val QUEEN = ChessPieceVal('Q', 9)
  val BISHOP = ChessPieceVal('B', 3)
  val KNIGHT = ChessPieceVal('N', 3)
  val ROOK = ChessPieceVal('R', 5)
  val PAWN = ChessPieceVal('P', 1)
  protected case class ChessPieceVal(character: Char, pointValue: Int) extends super.Val()
  implicit def convert(value: Value) = value.asInstanceOf[ChessPieceVal]
}

Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar (identisch mit der Liste für die direkte Verwendung der Java-Enumeration):

  1. 3.1 - Es wäre sehr schön, wenn ein Mitglied auch mit seinem Namen ohne Berücksichtigung der Groß- und Kleinschreibung abgerufen werden könnte
  2. 7 - Wenn Sie über Javas Enum hinausdenken, wäre es schön, Scalas Musterübereinstimmungsprüfung für eine Aufzählung explizit nutzen zu können

Auch für meine aktuellen Projekte ist Punkt 7 von entscheidender Bedeutung, damit ich Probleme mit der Kompilierungszeit erkennen kann, wenn ich Aufzählungselemente hinzufüge / entferne oder neuen Code für vorhandene Aufzählungsmitglieder schreibe.


Angesichts der obigen Definition einer Aufzählung funktioniert keine der drei oben genannten Lösungen, da sie nicht alles bieten, was in der obigen Aufzählungsdefinition beschrieben ist:

  1. Java Enum direkt in einem gemischten Scala / Java-Projekt
  2. "versiegeltes Merkmal + Fallobjekte"
  3. scala.Enumeration

Jede dieser Lösungen kann eventuell überarbeitet / erweitert / überarbeitet werden, um zu versuchen, einige der fehlenden Anforderungen jedes einzelnen zu decken. Weder Java Enumnoch die scala.EnumerationLösungen können jedoch ausreichend erweitert werden, um Punkt 7 bereitzustellen. Für meine eigenen Projekte ist dies einer der überzeugendsten Werte bei der Verwendung eines geschlossenen Typs in Scala. Ich bevorzuge Warnungen / Fehler zur Kompilierungszeit, um anzuzeigen, dass ich eine Lücke / ein Problem in meinem Code habe, anstatt es aus einer Ausnahme / einem Fehler zur Produktionslaufzeit herauslesen zu müssen.


In diesem Zusammenhang habe ich mich daran gemacht, mit dem case objectPfad zu arbeiten , um zu sehen, ob ich eine Lösung finden kann, die alle oben genannten Aufzählungsdefinitionen abdeckt. Die erste Herausforderung bestand darin, den Kern des Problems der JVM-Klassen- / Objektinitialisierung durchzuarbeiten (das in diesem StackOverflow-Beitrag ausführlich behandelt wird ). Und ich konnte endlich eine Lösung finden.

Als meine Lösung sind zwei Eigenschaften; Enumeration und EnumerationDecorated , und da das EnumerationMerkmal mehr als 400 Zeilen lang ist (viele Kommentare, die den Kontext erklären), verzichte ich darauf, es in diesen Thread einzufügen (was dazu führen würde, dass es die Seite erheblich ausdehnt). Für Details springen Sie bitte direkt zum Kern .

So sieht die Lösung aus, wenn dieselbe Datenidee wie oben (vollständig kommentierte Version hier verfügbar ) verwendet und in implementiert wird EnumerationDecorated.

import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.EnumerationDecorated

object ChessPiecesEnhancedDecorated extends EnumerationDecorated {
  case object KING extends Member
  case object QUEEN extends Member
  case object BISHOP extends Member
  case object KNIGHT extends Member
  case object ROOK extends Member
  case object PAWN extends Member

  val decorationOrderedSet: List[Decoration] =
    List(
        Decoration(KING,   'K', 0)
      , Decoration(QUEEN,  'Q', 9)
      , Decoration(BISHOP, 'B', 3)
      , Decoration(KNIGHT, 'N', 3)
      , Decoration(ROOK,   'R', 5)
      , Decoration(PAWN,   'P', 1)
    )

  final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase {
    val description: String = member.name.toLowerCase.capitalize
  }
  override def typeTagMember: TypeTag[_] = typeTag[Member]
  sealed trait Member extends MemberDecorated
}

Dies ist ein Beispiel für die Verwendung eines neuen Paares von Aufzählungsmerkmalen, die ich erstellt habe (in dieser Übersicht ), um alle in der Aufzählungsdefinition gewünschten und beschriebenen Funktionen zu implementieren.

Ein Anliegen ist, dass die Namen der Aufzählungsmitglieder wiederholt werden müssen ( decorationOrderedSetim obigen Beispiel). Obwohl ich es auf eine einzige Wiederholung reduziert habe, konnte ich aufgrund zweier Probleme nicht sehen, wie ich es noch weniger machen kann:

  1. Die JVM-Objekt- / Klasseninitialisierung für dieses bestimmte Objekt- / Fallobjektmodell ist undefiniert (siehe diesen Stackoverflow-Thread ).
  2. Der von der Methode zurückgegebene Inhalt getClass.getDeclaredClasseshat eine undefinierte Reihenfolge (und es ist ziemlich unwahrscheinlich, dass er in derselben Reihenfolge wie die case objectDeklarationen im Quellcode vorliegt).

Angesichts dieser beiden Probleme musste ich den Versuch aufgeben, eine implizite Bestellung zu generieren, und ich musste explizit verlangen, dass der Kunde sie mit einer Art geordnetem Satz definiert und deklariert. Da die Scala-Sammlungen keine Implementierung für geordnete Sätze haben, konnte ich am besten eine ListLaufzeitprüfung verwenden und dann überprüfen, ob es sich wirklich um einen Satz handelt. Es ist nicht so, wie ich es vorgezogen hätte, dies erreicht zu haben.

Und angesichts der Konstruktion dieser zweiten Liste / set Bestellung erforderlich val, angesichts der ChessPiecesEnhancedDecoratedobigen Beispiel war es möglich , hinzufügen case object PAWN2 extends Memberund dann vergessen , hinzuzufügen , Decoration(PAWN2,'P2', 2)zu decorationOrderedSet. Daher wird zur Laufzeit überprüft, ob die Liste nicht nur eine Menge ist, sondern ALLE Fallobjekte enthält, die die Liste erweitern sealed trait Member. Das war eine besondere Form der Reflexion / Makro-Hölle.


Bitte hinterlassen Sie Kommentare und / oder Feedback zum Kern .

chaotisches Gleichgewicht
quelle
Ich habe jetzt die erste Version der ScalaOlio-Bibliothek (GPLv3) veröffentlicht, die aktuellere Versionen von beiden org.scalaolio.util.Enumerationund org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium
Und um direkt zum ScalaOlio-Repository auf Github zu springen: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium
5
Dies ist eine qualitativ hochwertige Antwort und eine Menge zu nehmen. Vielen Dank
Anggabriel
1
Es sieht so aus, als ob Odersky Dotty (zukünftige Scala 3.0) mit einer nativen Aufzählung aktualisieren möchte. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium
62

Fallobjekte geben bereits ihren Namen für ihre toString-Methoden zurück, sodass eine separate Übergabe nicht erforderlich ist. Hier ist eine Version ähnlich der von jho (der Kürze halber wurden die Convenience-Methoden weggelassen):

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

Objekte sind faul; Wenn wir stattdessen vals verwenden, können wir die Liste löschen, müssen aber den Namen wiederholen:

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

Wenn Ihnen Betrug nichts ausmacht, können Sie Ihre Aufzählungswerte mithilfe der Reflection-API oder ähnlichem wie Google Reflections vorladen. Nicht faule Fallobjekte bieten Ihnen die sauberste Syntax:

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
}

Schön und sauber, mit allen Vorteilen von Fallklassen und Java-Aufzählungen. Persönlich definiere ich die Aufzählungswerte außerhalb des Objekts, um besser mit dem idiomatischen Scala-Code übereinzustimmen:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
GatesDA
quelle
3
Eine Frage: Die letzte Lösung heißt "nicht faule Fallobjekte", aber in diesem Fall werden Objekte erst geladen, wenn wir sie verwenden: Warum nennen Sie diese Lösung nicht faul?
Seb Cesbron
2
@Noel, Sie müssen Folgendes verwenden: Einfügen, um die gesamte versiegelte Hierarchie in die REPL einzufügen. Wenn Sie dies nicht tun, zählt die einzelne Zeile mit der versiegelten Basisklasse / dem versiegelten Merkmal als einzelne Datei, wird sofort versiegelt und kann nicht in der nächsten Zeile erweitert werden.
Jürgen Strobel
2
@GatesDA Nur Ihr erstes Code-Snippet weist keinen Fehler auf (da der Client ausdrücklich Werte deklarieren und definieren muss. Sowohl Ihre zweite als auch Ihre dritte Lösung weisen den subtilen Fehler auf, den ich in meinem letzten Kommentar beschrieben habe (falls der Client zufällig auf Currency zugreift) .GBP direkt und zuerst wird die Werteliste "außer Betrieb" sein. Ich habe die Scala-Aufzählungsdomäne ausführlich untersucht und sie in meiner Antwort auf denselben Thread ausführlich behandelt
chaotic3quilibrium
1
Möglicherweise besteht einer der Nachteile dieses Ansatzes (im Vergleich zu Java Enums sowieso) darin, dass bei der Eingabe von Currency <dot> in IDE keine verfügbaren Optionen angezeigt werden.
Ivan Balashov
1
Wie @SebCesbron erwähnt hat, sind die Fallobjekte hier faul. Wenn ich anrufe Currency.values, erhalte ich nur Werte zurück, auf die ich zuvor zugegriffen habe. Gibt es einen Weg daran vorbei?
Sasgorilla
27

Die Verwendung von Fallklassen gegenüber Aufzählungen bietet folgende Vorteile:

  • Bei Verwendung versiegelter Fallklassen kann der Scala-Compiler feststellen, ob die Übereinstimmung vollständig angegeben ist, z. B. wenn alle möglichen Übereinstimmungen in der Übereinstimmungsdeklaration angegeben sind. Bei Aufzählungen kann der Scala-Compiler dies nicht feststellen.
  • Fallklassen unterstützen natürlich mehr Felder als eine wertbasierte Aufzählung, die einen Namen und eine ID unterstützt.

Die Verwendung von Aufzählungen anstelle von Fallklassen bietet folgende Vorteile:

  • Aufzählungen sind im Allgemeinen etwas weniger Code zum Schreiben.
  • Aufzählungen sind für jemanden, der neu in Scala ist, etwas einfacher zu verstehen, da sie in anderen Sprachen verbreitet sind

Wenn Sie also nur eine Liste einfacher Konstanten nach Namen benötigen, verwenden Sie im Allgemeinen Aufzählungen. Wenn Sie andernfalls etwas Komplexeres benötigen oder die zusätzliche Sicherheit des Compilers Ihnen mitteilen möchte, ob alle Übereinstimmungen angegeben sind, verwenden Sie Fallklassen.

Aaron
quelle
15

UPDATE: Der folgende Code hat einen Fehler, der hier beschrieben wird . Das folgende Testprogramm funktioniert, aber wenn Sie DayOfWeek.Mon (zum Beispiel) vor DayOfWeek selbst verwenden, schlägt dies fehl, da DayOfWeek nicht initialisiert wurde (die Verwendung eines inneren Objekts bewirkt nicht, dass ein äußeres Objekt initialisiert wird). Sie können diesen Code weiterhin verwenden, wenn Sie etwas wie val enums = Seq( DayOfWeek )in Ihrer Hauptklasse tun und die Initialisierung Ihrer Aufzählungen erzwingen, oder Sie können die Modifikationen von chaotic3quilibrium verwenden. Ich freue mich auf eine makrobasierte Aufzählung!


Falls Sie es wollen

  • Warnungen vor nicht erschöpfenden Musterübereinstimmungen
  • Eine Int-ID, die jedem Aufzählungswert zugewiesen ist und die Sie optional steuern können
  • eine unveränderliche Liste der Enum-Werte in der Reihenfolge, in der sie definiert wurden
  • eine unveränderliche Karte vom Namen zum Aufzählungswert
  • eine unveränderliche Map von id bis enum value
  • Orte, an denen Methoden / Daten für alle oder bestimmte Aufzählungswerte oder für die gesamte Aufzählung festgehalten werden können
  • geordnete Aufzählungswerte (damit Sie beispielsweise testen können, ob Tag <Mittwoch ist)
  • die Fähigkeit, eine Aufzählung zu erweitern, um andere zu erstellen

dann kann das Folgende von Interesse sein. Feedback willkommen.

In dieser Implementierung gibt es abstrakte Enum- und EnumVal-Basisklassen, die Sie erweitern. Wir werden diese Klassen in einer Minute sehen, aber zuerst würden Sie eine Aufzählung folgendermaßen definieren:

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

Beachten Sie, dass Sie jeden Enum-Wert verwenden müssen (rufen Sie die Apply-Methode auf), um ihn zum Leben zu erwecken. [Ich wünschte, innere Objekte wären nicht faul, es sei denn, ich fordere sie ausdrücklich dazu auf. Meiner Ansicht nach.]

Wir könnten natürlich Methoden / Daten zu DayOfWeek, Val oder den einzelnen Fallobjekten hinzufügen, wenn wir dies wünschen.

Und so würden Sie eine solche Aufzählung verwenden:

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

}

Folgendes erhalten Sie beim Kompilieren:

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

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

Sie können "Tagesübereinstimmung" durch "(Tag: @unchecked) Übereinstimmung" ersetzen, wenn Sie solche Warnungen nicht möchten, oder am Ende einfach einen Sammelfall einfügen.

Wenn Sie das obige Programm ausführen, erhalten Sie folgende Ausgabe:

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

Da die Liste und die Karten unveränderlich sind, können Sie Elemente leicht entfernen, um Teilmengen zu erstellen, ohne die Aufzählung selbst zu beschädigen.

Hier ist die Enum-Klasse selbst (und EnumVal darin):

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

}

Und hier ist eine fortgeschrittenere Verwendung, die die IDs steuert und Daten / Methoden zur Val-Abstraktion und zur Aufzählung selbst hinzufügt:

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)
}
AmigoNico
quelle
Tyvm für die Bereitstellung. Ich weiß das wirklich zu schätzen. Ich bemerke jedoch, dass es "var" im Gegensatz zu val verwendet. Und dies ist eine grenzwertige Todsünde in der FP-Welt. Gibt es also eine Möglichkeit, dies so zu implementieren, dass var nicht verwendet wird? Ich bin nur neugierig, ob dies eine Art FP-Randfall ist, und ich verstehe nicht, wie Ihre Implementierung FP unerwünscht ist.
chaotisches Gleichgewicht
2
Ich kann dir wahrscheinlich nicht helfen. In Scala ist es ziemlich üblich, Klassen zu schreiben, die intern mutieren, aber für diejenigen, die sie verwenden, unveränderlich sind. Im obigen Beispiel kann ein Benutzer von DayOfWeek die Aufzählung nicht ändern. Es gibt beispielsweise keine Möglichkeit, die ID von Dienstag oder ihren Namen nachträglich zu ändern. Aber wenn Sie eine Implementierung wollen, die intern frei von Mutationen ist , dann habe ich nichts. Es würde mich jedoch nicht wundern, eine schöne neue Enum-Funktion zu sehen, die auf Makros in 2.11 basiert. Ideen werden auf Scala-Lang herumgetrampelt.
AmigoNico
Ich erhalte einen seltsamen Fehler im Scala-Arbeitsblatt. Wenn ich eine der Value-Instanzen direkt verwende, wird ein Initialisierungsfehler angezeigt. Wenn ich jedoch die .values-Methode aufrufe, um den Inhalt der Aufzählung anzuzeigen, funktioniert dies und dann funktioniert die direkte Verwendung der Wertinstanz. Irgendeine Idee, was der Initialisierungsfehler ist? Und was ist der optimale Weg, um sicherzustellen, dass die Initialisierung unabhängig von der Aufrufkonvention in der richtigen Reihenfolge erfolgt?
chaotic3quilibrium
@ chaotic3quilibrium: Wow! Danke, dass Sie dies weiterverfolgen, und natürlich danke an Rex Kerr für das schwere Heben. Ich werde das Problem hier erwähnen und mich auf die von Ihnen erstellte Frage beziehen.
AmigoNico
"[Verwenden var] ist eine grenzwertige Todsünde in der FP-Welt" - Ich glaube nicht, dass die Meinung allgemein akzeptiert wird.
Erik Kaplun
12

Ich habe hier eine schöne einfache Bibliothek, mit der Sie versiegelte Merkmale / Klassen als Enum-Werte verwenden können, ohne Ihre eigene Werteliste führen zu müssen. Es basiert auf einem einfachen Makro, das nicht vom Buggy abhängig ist knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum

lloydmeta
quelle
10

Update März 2017: Wie von Anthony Accioly kommentiert , wurde die scala.Enumeration/enumPR geschlossen.

Dotty (Compiler der nächsten Generation für Scala) wird die Führung übernehmen, obwohl Dotty 1970 und Martin Oderskys PR 1958 .


Hinweis: Es gibt jetzt (August 2016, 6+ Jahre später) einen Vorschlag zum Entfernen scala.Enumeration: PR 5352

Veraltet scala.Enumeration, hinzufügen@enum Anmerkung

Die Syntax

@enum
 class Toggle {
  ON
  OFF
 }

ist ein mögliches Implementierungsbeispiel. Es ist beabsichtigt, auch ADTs zu unterstützen, die bestimmten Einschränkungen entsprechen (keine Verschachtelung, Rekursion oder Variation der Konstruktorparameter), z.

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

Lehnt die absolute Katastrophe ab scala.Enumeration .

Vorteile von @enum gegenüber scala.Enumeration:

  • Funktioniert eigentlich
  • Java Interop
  • Keine Löschprobleme
  • Kein verwirrendes Mini-DSL-Lernen beim Definieren von Aufzählungen

Nachteile: Keine.

Dies behebt das Problem, dass nicht eine Codebasis vorhanden sein kann, die Scala-JVM Scala.jsund Scala-Native unterstützt (Java-Quellcode wird nicht unterstützt Scala.js/Scala-Native, Scala-Quellcode kann keine Aufzählungen definieren, die von vorhandenen APIs in Scala-JVM akzeptiert werden).

VonC
quelle
Die PR oben hat geschlossen (keine Freude). Es ist jetzt 2017 und es sieht so aus, als würde Dotty endlich ein Enum-Konstrukt bekommen. Hier ist das Problem und Martins PR . Zusammenführen, zusammenführen, zusammenführen!
Anthony Accioly
8

Ein weiterer Nachteil von Fallklassen gegenüber Aufzählungen, wenn Sie über alle Instanzen hinweg iterieren oder filtern müssen. Dies ist eine integrierte Funktion der Aufzählung (und auch der Java-Aufzählungen), während Fallklassen diese Funktion nicht automatisch unterstützen.

Mit anderen Worten: "Es gibt keine einfache Möglichkeit, eine Liste der gesamten Aufzählungswerte mit Fallklassen abzurufen."

user142435
quelle
5

Wenn Sie es ernst meinen, die Interoperabilität mit anderen JVM-Sprachen (z. B. Java) aufrechtzuerhalten, ist es am besten, Java-Enums zu schreiben. Diese arbeiten transparent sowohl mit Scala- als auch mit Java-Code, was mehr ist, als für scala.Enumerationoder Fallobjekte gesagt werden kann. Lassen Sie uns nicht für jedes neue Hobbyprojekt auf GitHub eine neue Aufzählungsbibliothek haben, wenn dies vermieden werden kann!

Connor Doyle
quelle
4

Ich habe verschiedene Versionen gesehen, in denen eine Fallklasse eine Aufzählung nachahmt. Hier ist meine Version:

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

Auf diese Weise können Sie Fallklassen erstellen, die wie folgt aussehen:

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

Vielleicht könnte sich jemand einen besseren Trick einfallen lassen, als einfach eine Fallklasse wie ich zur Liste hinzuzufügen. Das war alles, was ich mir damals einfallen lassen konnte.

jho
quelle
Warum zwei getrennte Methoden?
Saish
@jho Ich habe versucht, Ihre Lösung so wie sie ist durchzuarbeiten, aber sie wird nicht kompiliert. Im zweiten Code-Snippit gibt es einen Verweis auf Site in "Typ V = Site". Ich bin mir nicht sicher, worauf sich das bezieht, um den Kompilierungsfehler zu beheben. Warum stellen Sie als Nächstes die leeren Klammern für "abstrakte Klassenwährung" bereit? Könnten sie nicht einfach weggelassen werden? Warum verwenden Sie eine Variable in "var values ​​= ..."? Bedeutet dies nicht, dass Clients jederzeit von überall im Code eine neue Liste den Werten zuweisen können? Wäre es nicht weitaus besser, es zu einem Wert anstelle eines Var zu machen?
chaotic3quilibrium
2

Ich habe diese beiden Optionen die letzten Male hin und her gegangen, als ich sie gebraucht habe. Bis vor kurzem habe ich die Option für versiegelte Merkmale / Fallobjekte bevorzugt.

1) Erklärung zur Scala-Aufzählung

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Versiegelte Eigenschaften + Fallobjekte

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Während keines von beiden wirklich alle Anforderungen einer Java-Aufzählung erfüllt, sind im Folgenden die Vor- und Nachteile aufgeführt:

Scala-Aufzählung

Vorteile: -Funktionen zum Instanziieren mit Option oder zur direkten Annahme einer genauen (einfacher beim Laden aus einem persistenten Speicher) -Iteration über alle möglichen Werte wird unterstützt

Nachteile: -Kompilierungswarnung für nicht erschöpfende Suche wird nicht unterstützt (macht Mustervergleich weniger ideal)

Fallobjekte / Versiegelte Merkmale

Vorteile: - Mit versiegelten Merkmalen können wir einige Werte vorab instanziieren, während andere zum Zeitpunkt der Erstellung injiziert werden können. - Volle Unterstützung für den Mustervergleich (definierte Methoden anwenden / nicht anwenden).

Nachteile: - Ausgehend von einem persistenten Speicher - Sie müssen hier häufig den Mustervergleich verwenden oder eine eigene Liste aller möglichen 'Aufzählungswerte' definieren.

Was mich letztendlich dazu brachte, meine Meinung zu ändern, war so etwas wie der folgende Ausschnitt:

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
}

Die .getAufrufe waren abscheulich - stattdessen kann ich mit der Aufzählung einfach die withName-Methode für die Aufzählung wie folgt aufrufen:

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

Daher denke ich, dass ich es in Zukunft vorziehen werde, Aufzählungen zu verwenden, wenn auf die Werte aus einem Repository zugegriffen werden soll, und ansonsten Fallobjekte / versiegelte Merkmale.

Tollwütiger Hund
quelle
Ich kann sehen, wie wünschenswert das zweite Codemuster ist (die beiden Hilfsmethoden aus dem ersten Codemuster entfernen). Ich habe jedoch herausgefunden, wie Sie nicht gezwungen sind, zwischen diesen beiden Mustern zu wählen. Ich decke die gesamte Domain in der Antwort ab, die ich in diesem Thread gepostet habe: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
2

Ich bevorzuge case objects(es ist eine Frage der persönlichen Präferenz). Um die mit diesem Ansatz verbundenen Probleme zu bewältigen (Zeichenfolge analysieren und über alle Elemente iterieren), habe ich einige Zeilen hinzugefügt, die nicht perfekt, aber effektiv sind.

Ich füge Ihnen den Code hier ein und erwarte, dass er nützlich sein könnte und dass andere ihn verbessern könnten.

/**
 * 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 }
}
Jaguililla
quelle
0

Für diejenigen, die noch suchen, wie GatesDas Antwort funktioniert : Sie können einfach auf das case-Objekt verweisen, nachdem Sie es deklariert haben, um es zu instanziieren:

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
}
V-Lampe
quelle
0

Ich denke, der größte Vorteil von case classesOver enumerationsist, dass Sie Typklassenmuster, auch Ad-hoc-Polymorphysmus genannt, verwenden können . Sie müssen keine Aufzählungen wie:

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

Stattdessen haben Sie so etwas wie:

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() = ...
}
Murat Mustafin
quelle