Objets de cas vs énumérations à Scala


231

Existe-t-il des directives de bonnes pratiques sur le moment d'utiliser des classes de cas (ou des objets de cas) par rapport à l'extension de l'énumération dans Scala?

Ils semblent offrir certains des mêmes avantages.


2
J'ai écrit un petit aperçu de l'énumération scala et des alternatives, vous pouvez le trouver utile: pedrorijo.com/blog/scala-enums/
pedrorijo91

1
Voir également Scala 3 basé à Dottyenum (pour mi 2020).
VonC

Réponses:


223

Une grande différence est que les Enumerations viennent avec un support pour les instancier à partir d'une namechaîne. Par exemple:

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

Ensuite, vous pouvez faire:

val ccy = Currency.withName("EUR")

Cela est utile lorsque vous souhaitez conserver des énumérations (par exemple, dans une base de données) ou les créer à partir de données résidant dans des fichiers. Cependant, je trouve en général que les énumérations sont un peu maladroites dans Scala et ont la sensation d'un add-on maladroit, donc j'ai maintenant tendance à utiliser l' case objectart. A case objectest plus flexible qu'une énumération:

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

case class UnknownCurrency(name: String) extends Currency

Alors maintenant, j'ai l'avantage de ...

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

Comme l'a souligné @ chaotic3quilibrium (avec quelques corrections pour faciliter la lecture):

En ce qui concerne le modèle "UnknownCurrency (code)", il existe d'autres moyens de ne pas trouver une chaîne de code de devise que de "casser" la nature de l'ensemble fermé du Currencytype. UnknownCurrencyêtre de type Currencypeut désormais se faufiler dans d'autres parties d'une API.

Il est conseillé de pousser ce cas à l'extérieur Enumerationet de faire en sorte que le client traite un Option[Currency]type qui indiquerait clairement qu'il y a vraiment un problème de correspondance et "encourage" l'utilisateur de l'API à le résoudre lui-même.

Pour poursuivre sur les autres réponses ici, les principaux inconvénients de case objects sur Enumerations sont:

  1. Impossible d'itérer sur toutes les instances de "l'énumération" . C'est certainement le cas, mais j'ai trouvé extrêmement rare dans la pratique que cela soit nécessaire.

  2. Impossible d'instancier facilement à partir d'une valeur persistante . Cela est également vrai, mais, sauf dans le cas d'énumérations énormes (par exemple, toutes les devises), cela ne présente pas d'énormes frais généraux.


10
L'autre différence est que l'énumération d'énumération est commandée hors de la boîte, tandis que l'énumération basée sur l'objet de cas n'est pas évidente
om-nom-nom

1
Un autre point pour les objets de cas est si vous vous souciez de l'interopérabilité java. L'énumération renverrait les valeurs sous la forme Enumeration.Value, donc 1) nécessitant scala-library, 2) perdant les informations de type réelles.
juanmirocks

7
@oxbow_lakes En ce qui concerne le point 1, en particulier cette partie "... J'ai trouvé extrêmement rare dans la pratique que cela soit nécessaire": Apparemment, vous faites rarement beaucoup de travail sur l'interface utilisateur. Il s'agit d'un cas d'utilisation extrêmement courant; affichage d'une liste (déroulante) de membres d'énumération valides parmi lesquels effectuer la sélection.
chaotic3quilibrium

Je ne comprends pas le type d'élément correspondant trade.ccydans l'exemple de trait scellé.
rloth

et ne case objectgénère pas une empreinte de code plus grande (~ 4x) que Enumeration? Distinction utile en particulier pour les scala.jsprojets nécessitant une faible empreinte.
ecoe

69

MISE À JOUR: Une nouvelle solution basée sur des macros a été créée, bien supérieure à la solution que je décris ci-dessous. Je recommande fortement d'utiliser cette nouvelle solution basée sur des macros . Et il semble que Dotty fera de ce style de solution d'énumération une partie du langage. Whoo Hoo!

Résumé:
Il existe trois modèles de base pour tenter de reproduire le Java Enumdans un projet Scala. Deux des trois modèles; en utilisant directement Java Enumet scala.Enumeration, ne sont pas capables d'activer la correspondance de motifs exhaustive de Scala. Et le troisième; "trait scellé + objet cas", fait ... mais a des complications d'initialisation de classe / objet JVM entraînant une génération d'index ordinal incohérente.

J'ai créé une solution avec deux classes; Enumeration and EnumerationDecorated , situé dans ce Gist . Je n'ai pas posté le code dans ce fil car le fichier pour l'énumération était assez volumineux (+400 lignes - contient beaucoup de commentaires expliquant le contexte d'implémentation).

Détails:
La question que vous posez est assez générale; "... quand utiliser les caseclassesobjects vs étendre [scala.]Enumeration". Et il s'avère qu'il y a BEAUCOUP de réponses possibles, chaque réponse dépendant des subtilités des exigences spécifiques du projet que vous avez. La réponse peut être réduite à trois modèles de base.

Pour commencer, assurons-nous que nous travaillons à partir de la même idée de base de ce qu'est une énumération. Définissons une énumération principalement en termes de Enumfourni à partir de Java 5 (1.5) :

  1. Il contient un ensemble fermé naturellement ordonné de membres nommés
    1. Il y a un nombre fixe de membres
    2. Les membres sont naturellement classés et explicitement indexés
      • Plutôt que d'être trié en fonction de certains critères de recherche des membres inates
    3. Chaque membre a un nom unique dans l'ensemble total de tous les membres
  2. Tous les membres peuvent facilement être itérés en fonction de leurs index
  3. Un membre peut être récupéré avec son nom (sensible à la casse)
    1. Ce serait bien si un membre pouvait également être récupéré avec son nom insensible à la casse
  4. Un membre peut être récupéré avec son index
  5. Les membres peuvent facilement, de manière transparente et efficace utiliser la sérialisation
  6. Les membres peuvent être facilement étendus pour contenir des données de singleton associées supplémentaires
  7. Au-delà de Java Enum, il serait bien de pouvoir utiliser explicitement le modèle de Scala correspondant à l'exhaustivité en vérifiant une énumération

Ensuite, regardons les versions résumées des trois modèles de solution les plus courants publiés:

A) En fait, en utilisant directement le modèle JavaEnum (dans un projet mixte Scala / Java):

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

Les éléments suivants de la définition d'énumération ne sont pas disponibles:

  1. 3.1 - Ce serait très bien si un membre pouvait également être récupéré avec son nom insensible à la casse
  2. 7 - En pensant au-delà de Java's Enum, il serait bien de pouvoir utiliser explicitement le modèle Scala correspondant à l'exhaustivité en vérifiant une énumération

Pour mes projets en cours, je n'ai pas l'avantage de prendre des risques autour du parcours mixte Scala / Java. Et même si je pouvais choisir de faire un projet mixte, l'élément 7 est essentiel pour me permettre d'attraper les problèmes de temps de compilation si / quand j'ajoute / supprime des membres d'énumération, ou j'écris du nouveau code pour traiter les membres d'énumération existants.


B) En utilisant le modèle " sealed trait+case objects ":

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

Les éléments suivants de la définition d'énumération ne sont pas disponibles:

  1. 1.2 - Les membres sont naturellement ordonnés et explicitement indexés
  2. 2 - Tous les membres peuvent facilement être itérés en fonction de leurs index
  3. 3 - Un membre peut être récupéré avec son nom (sensible à la casse)
  4. 3.1 - Ce serait très bien si un membre pouvait également être récupéré avec son nom insensible à la casse
  5. 4 - Un membre peut être récupéré avec son index

On peut soutenir qu'il répond vraiment aux éléments de définition de l'énumération 5 et 6. Pour 5, c'est un tronçon de prétendre qu'il est efficace. Pour 6, il n'est pas vraiment facile d'étendre pour contenir des données de singleton associées supplémentaires.


C) En utilisant le scala.Enumerationmodèle (inspiré de cette réponse StackOverflow ):

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

Les éléments suivants de la définition d'énumération ne sont pas disponibles (il se trouve qu'ils sont identiques à la liste pour l'utilisation directe de l'énumération Java):

  1. 3.1 - Ce serait très bien si un membre pouvait également être récupéré avec son nom insensible à la casse
  2. 7 - En pensant au-delà de Java's Enum, il serait bien de pouvoir utiliser explicitement le modèle Scala correspondant à l'exhaustivité en vérifiant une énumération

Encore une fois pour mes projets en cours, l'élément 7 est essentiel pour me permettre d'attraper des problèmes de temps de compilation si / quand j'ajoute / supprime des membres d'énumération, ou si j'écris du nouveau code pour traiter les membres d'énumération existants.


Donc, étant donné la définition ci-dessus d'une énumération, aucune des trois solutions ci-dessus ne fonctionne car elles ne fournissent pas tout ce qui est décrit dans la définition d'énumération ci-dessus:

  1. Java Enum directement dans un projet mixte Scala / Java
  2. "trait scellé + objets de cas"
  3. scala.Enumeration

Chacune de ces solutions peut éventuellement être retravaillée / étendue / refactorisée pour tenter de couvrir certaines des exigences manquantes de chacun. Cependant, ni Java Enumni les scala.Enumerationsolutions ne peuvent être suffisamment développées pour fournir l'élément 7. Et pour mes propres projets, c'est l'une des valeurs les plus convaincantes de l'utilisation d'un type fermé dans Scala. Je préfère fortement les erreurs / avertissements de temps de compilation pour indiquer que j'ai un écart / problème dans mon code plutôt que d'avoir à le glaner d'une exception / échec d'exécution de production.


À cet égard, je me suis mis à travailler avec la case objectvoie pour voir si je pouvais produire une solution qui couvrait toute la définition d'énumération ci-dessus. Le premier défi a été de passer à travers le cœur du problème d'initialisation de classe / objet JVM (traité en détail dans cet article StackOverflow ). Et j'ai finalement pu trouver une solution.

Comme ma solution est deux traits; Enumeration et EnumerationDecorated , et puisque le Enumerationtrait fait plus de +400 lignes (beaucoup de commentaires expliquant le contexte), je renonce à le coller dans ce fil (ce qui le ferait considérablement descendre la page). Pour plus de détails, veuillez passer directement à l' essentiel .

Voici à quoi ressemble la solution en utilisant la même idée de données que ci-dessus (version entièrement commentée disponible ici ) et implémentée dans 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
}

Ceci est un exemple d'utilisation d'une nouvelle paire de traits d'énumération que j'ai créée (située dans cet Gist ) pour implémenter toutes les capacités souhaitées et décrites dans la définition d'énumération.

Une préoccupation exprimée est que les noms des membres de l'énumération doivent être répétés ( decorationOrderedSetdans l'exemple ci-dessus). Bien que je l'ai réduit à une seule répétition, je ne voyais pas comment le rendre encore moins en raison de deux problèmes:

  1. L'initialisation d'objet / classe JVM pour ce modèle d'objet / objet particulier n'est pas définie (voir ce fil Stackoverflow )
  2. Le contenu renvoyé par la méthode getClass.getDeclaredClassesa un ordre non défini (et il est peu probable qu'il soit dans le même ordre que les case objectdéclarations dans le code source)

Compte tenu de ces deux problèmes, j'ai dû renoncer à générer un ordre implicite et j'ai dû explicitement demander au client de le définir et de le déclarer avec une sorte de notion d'ensemble ordonné. Comme les collections Scala n'ont pas d'implémentation d'ensemble d'insertions ordonnées, le mieux que je pouvais faire était d'utiliser un Listet puis de vérifier à l'exécution qu'il s'agissait vraiment d'un ensemble. Ce n'est pas comme ça que j'aurais préféré y arriver.

Et étant donné la conception nécessaire cette deuxième liste / commande ensemble val, compte tenu de l' ChessPiecesEnhancedDecoratedexemple ci - dessus, il est possible d'ajouter case object PAWN2 extends Member, puis oublier d'ajouter Decoration(PAWN2,'P2', 2)à decorationOrderedSet. Ainsi, il y a un contrôle d'exécution pour vérifier que la liste n'est pas seulement un ensemble, mais contient TOUS les objets de cas qui étendent le sealed trait Member. C'était une forme spéciale de réflexion / macro enfer à travailler.


Veuillez laisser des commentaires et / ou des commentaires sur le Gist .


J'ai maintenant publié la première version de la bibliothèque ScalaOlio (GPLv3) qui contient des versions plus à jour des deux org.scalaolio.util.Enumerationet org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium

Et pour accéder directement au référentiel ScalaOlio sur Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium

5
C'est une réponse de qualité et beaucoup à en tirer. Merci
angabriel

1
Il semble que Odersky souhaite mettre à jour Dotty (future Scala 3.0) avec une énumération native. Whoo Hoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium

62

Les objets Case renvoient déjà leur nom pour leurs méthodes toString, il n'est donc pas nécessaire de les transmettre séparément. Voici une version similaire à celle de jho (méthodes de commodité omises par souci de concision):

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

Les objets sont paresseux; en utilisant vals à la place, nous pouvons supprimer la liste, mais nous devons répéter le nom:

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

Si cela ne vous dérange pas, vous pouvez précharger vos valeurs d'énumération en utilisant l'API de réflexion ou quelque chose comme Google Reflections. Les objets cas non paresseux vous offrent la syntaxe la plus nette:

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
}

Agréable et propre, avec tous les avantages des classes de cas et des énumérations Java. Personnellement, je définis les valeurs d'énumération en dehors de l'objet pour mieux correspondre au code Scala idiomatique:

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

3
une question: la dernière solution est appelée "objets cas non paresseux" mais dans ce cas, les objets ne sont pas chargés jusqu'à ce que nous les utilisons: pourquoi appelez-vous cette solution non paresseux?
Seb Cesbron

2
@Noel, vous devez utiliser: paste pour coller toute la hiérarchie scellée dans le REPL. Si vous ne le faites pas, la ligne unique avec la classe / trait de base scellé compte comme un seul fichier, est scellée immédiatement et ne peut pas être étendue sur la ligne suivante.
Jürgen Strobel

2
@GatesDA Seul votre premier extrait de code n'a pas de bogue (car vous demandez explicitement au client de déclarer et de définir des valeurs. Vos deuxième et troisième solutions ont le bogue subtil que j'ai décrit dans mon dernier commentaire (si le client accède à Currency) .GBP directement et d'abord, la liste des valeurs sera "hors service". J'ai exploré le domaine d'énumération Scala de manière approfondie et l'ai couvert en détail dans ma réponse à ce même sujet: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

1
Peut-être que l'un des inconvénients de cette approche (par rapport à Java Enums de toute façon) est que lorsque vous tapez Currency <dot> dans IDE, il n'affiche pas les options disponibles.
Ivan Balashov

1
Comme @SebCesbron l'a mentionné, les objets de cas sont paresseux ici. Donc, si j'appelle Currency.values, je ne récupère que les valeurs auxquelles j'ai déjà accédé. Y a-t-il un moyen de contourner cela?
Sasgorilla

27

Les avantages de l'utilisation des classes de cas par rapport aux énumérations sont les suivants:

  • Lors de l'utilisation de classes de cas scellés, le compilateur Scala peut dire si la correspondance est entièrement spécifiée, par exemple lorsque toutes les correspondances possibles sont intégrées dans la déclaration de correspondance. Avec les énumérations, le compilateur Scala ne peut pas le dire.
  • Les classes de cas prennent naturellement en charge plus de champs qu'une énumération basée sur une valeur qui prend en charge un nom et un ID.

Les avantages de l'utilisation des énumérations au lieu des classes de cas sont les suivants:

  • Les énumérations seront généralement un peu moins de code à écrire.
  • Les énumérations sont un peu plus faciles à comprendre pour quelqu'un de nouveau à Scala car elles sont répandues dans d'autres langues

Donc, en général, si vous avez juste besoin d'une liste de constantes simples par nom, utilisez des énumérations. Sinon, si vous avez besoin de quelque chose d'un peu plus complexe ou si vous voulez la sécurité supplémentaire du compilateur vous indiquant si toutes les correspondances sont spécifiées, utilisez des classes de cas.


15

MISE À JOUR: Le code ci-dessous a un bug, décrit ici . Le programme de test ci-dessous fonctionne, mais si vous deviez utiliser DayOfWeek.Mon (par exemple) avant DayOfWeek lui-même, il échouerait car DayOfWeek n'a pas été initialisé (l'utilisation d'un objet interne ne provoque pas l'initialisation d'un objet externe). Vous pouvez toujours utiliser ce code si vous faites quelque chose comme val enums = Seq( DayOfWeek )dans votre classe principale, forçant l'initialisation de vos énumérations, ou vous pouvez utiliser les modifications de chaotic3quilibrium. Dans l'attente d'une énumération basée sur les macros!


Si tu veux

  • avertissements sur les correspondances de motifs non exhaustives
  • un ID int attribué à chaque valeur d'énumération, que vous pouvez éventuellement contrôler
  • une liste immuable des valeurs d'énumération, dans l'ordre où elles ont été définies
  • une carte immuable du nom à la valeur énumérée
  • une carte immuable de l'id à la valeur énumérée
  • des endroits où coller des méthodes / données pour toutes ou certaines valeurs d'énumération, ou pour l'énumération dans son ensemble
  • valeurs énumérées ordonnées (afin que vous puissiez tester, par exemple, si le jour <mercredi)
  • la possibilité d'étendre une énumération pour en créer d'autres

alors ce qui suit peut être intéressant. Commentaires bienvenus.

Dans cette implémentation, il existe des classes de base Enum et EnumVal abstraites que vous étendez. Nous verrons ces classes dans une minute, mais d'abord, voici comment vous définiriez une énumération:

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

Notez que vous devez utiliser chaque valeur d'énumération (appelez sa méthode apply) pour lui donner vie. [Je souhaite que les objets intérieurs ne soient pas paresseux à moins que je ne le demande spécifiquement. Je pense.]

Nous pouvons bien sûr ajouter des méthodes / données à DayOfWeek, Val ou aux objets de cas individuels si nous le souhaitons.

Et voici comment vous utiliseriez une telle énumération:

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

}

Voici ce que vous obtenez lorsque vous le compilez:

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

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

Vous pouvez remplacer "match du jour" par "(jour: @unchecked) match" où vous ne voulez pas de tels avertissements, ou simplement inclure un cas fourre-tout à la fin.

Lorsque vous exécutez le programme ci-dessus, vous obtenez cette sortie:

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

Notez que puisque la liste et les cartes sont immuables, vous pouvez facilement supprimer des éléments pour créer des sous-ensembles, sans casser l'énumération elle-même.

Voici la classe Enum elle-même (et EnumVal en son sein):

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

}

Et voici une utilisation plus avancée de celui-ci qui contrôle les ID et ajoute des données / méthodes à l'abstraction Val et à l'énumération elle-même:

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

Tyvm pour avoir fourni cela. J'apprécie vraiment cela. Cependant, je remarque qu'il utilise "var" par opposition à val. Et ceci est un péché mortel limite dans le monde FP. Alors, existe-t-il un moyen de l'implémenter de telle sorte qu'il n'y ait pas d'utilisation de var? Juste curieux de savoir s'il s'agit d'une sorte de cas de bord de type FP et je ne comprends pas comment votre mise en œuvre est FP indésirable.
chaotic3quilibrium

2
Je ne peux probablement pas t'aider. Il est assez courant dans Scala d'écrire des classes qui muent en interne mais qui sont immuables pour ceux qui les utilisent. Dans l'exemple ci-dessus, un utilisateur de DayOfWeek ne peut pas muter l'énumération; il n'y a aucun moyen, par exemple, de changer l'ID de mardi, ou son nom, après coup. Mais si vous voulez une implémentation sans mutation interne , je n'ai rien. Je ne serais pas surpris, cependant, de voir une belle nouvelle installation d'énumération basée sur des macros en 2.11; des idées sont lancées sur scala-lang.
AmigoNico

Je reçois une erreur étrange dans la feuille de calcul Scala. Si j'utilise directement l'une des instances Value, j'obtiens une erreur d'initialisation. Cependant, si je lance un appel à la méthode .values ​​pour voir le contenu de l'énumération, cela fonctionne, puis j'utilise directement l'instance de valeur. Une idée de l'erreur d'erreur d'initialisation? Et quelle est la meilleure façon de s'assurer que l'initialisation se produit dans le bon ordre, quelle que soit la convention d'appel?
chaotic3quilibrium

@ chaotic3quilibrium: Wow! Merci d'avoir poursuivi cela, et bien sûr merci à Rex Kerr pour le gros du travail. Je mentionnerai le problème ici et ferai référence à la question que vous avez créée.
AmigoNico

"[Utiliser var] est un péché mortel limite dans le monde de la PF" - je ne pense pas que l'opinion soit universellement acceptée.
Erik Kaplun

12

J'ai une belle bibliothèque simple ici qui vous permet d'utiliser des traits / classes scellés comme valeurs d'énumération sans avoir à maintenir votre propre liste de valeurs. Il s'appuie sur une simple macro qui ne dépend pas du buggyknownDirectSubclasses .

https://github.com/lloydmeta/enumeratum


10

Mise à jour mars 2017: comme l'a commenté Anthony Accioly , le scala.Enumeration/enumPR a été fermé.

Dotty (compilateur de nouvelle génération pour Scala) prendra la tête, bien que le numéro de dotty 1970 et le PR 1958 de Martin Odersky .


Remarque: il y a maintenant (août 2016, 6 ans et plus plus tard) une proposition de suppression scala.Enumeration: PR 5352

Déprécier scala.Enumeration, ajouter@enum annotation

La syntaxe

@enum
 class Toggle {
  ON
  OFF
 }

est un exemple d'implémentation possible, l'intention est également de prendre en charge les ADT conformes à certaines restrictions (pas d'imbrication, de récursivité ou de paramètres de constructeur variables), par exemple:

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

Déprécie la catastrophe non atténuée qui est scala.Enumeration .

Avantages de @enum par rapport à scala.

  • Fonctionne réellement
  • Java interop
  • Aucun problème d'effacement
  • Pas de mini-DSL déroutant à apprendre lors de la définition des énumérations

Inconvénients: aucun.

Cela résout le problème de ne pas pouvoir avoir une base de code prenant en charge Scala-JVM Scala.jset Scala-Native (code source Java non pris en charge sur Scala.js/Scala-Native, code source Scala ne pouvant pas définir d'énumérations acceptées par les API existantes sur Scala-JVM).


Le PR ci-dessus a fermé (pas de joie). Nous sommes maintenant en 2017 et il semble que Dotty va enfin obtenir une construction enum. Voici le problème et le PR de Martin . Fusionnez, fusionnez, fusionnez!
Anthony Accioly

8

Un autre inconvénient des classes de cas par rapport aux énumérations lorsque vous devrez itérer ou filtrer sur toutes les instances. Il s'agit d'une capacité intégrée d'énumération (et d'énumérations Java également) tandis que les classes de cas ne prennent pas automatiquement en charge une telle capacité.

En d'autres termes: "il n'y a pas de moyen facile d'obtenir une liste de l'ensemble total des valeurs énumérées avec les classes de cas".


5

Si vous souhaitez sérieusement maintenir l'interopérabilité avec d'autres langages JVM (par exemple Java), la meilleure option est d'écrire des énumérations Java. Ceux-ci fonctionnent de manière transparente à partir du code Scala et Java, ce qui est plus que ce qui peut être dit pour les scala.Enumerationobjets ou les cas. N'ayons pas une nouvelle bibliothèque d'énumérations pour chaque nouveau projet de passe-temps sur GitHub, si cela peut être évité!


4

J'ai vu différentes versions de faire une classe de cas imiter une énumération. Voici ma 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)
    }
}

Ce qui vous permet de construire des classes de cas qui ressemblent à ceci:

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

Peut-être que quelqu'un pourrait trouver une meilleure astuce que d'ajouter simplement une classe de cas à la liste comme je l'ai fait. C'est tout ce que j'ai pu trouver à l'époque.


Pourquoi deux méthodes distinctes de non-application?
Saish

@jho J'ai essayé de résoudre votre solution telle quelle, mais elle ne se compilera pas. Dans le deuxième extrait de code, il y a une référence à Site dans "type V = Site". Je ne sais pas trop à quoi cela fait référence pour éliminer l'erreur de compilation. Ensuite, pourquoi fournissez-vous les accolades vides pour "devise de classe abstraite"? Ne pouvaient-ils pas simplement être laissés de côté? Enfin, pourquoi utilisez-vous un var dans "var values ​​= ..."? Cela ne signifie-t-il pas que les clients peuvent à tout moment de n'importe où dans le code attribuer une nouvelle liste aux valeurs? Ne serait-il pas préférable d'en faire un val au lieu d'un var?
chaotic3quilibrium

2

J'ai fait des allers-retours sur ces deux options les dernières fois où j'en ai eu besoin. Jusqu'à récemment, ma préférence a été pour l'option d'objet trait / cas scellé.

1) Déclaration de dénombrement Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Traits scellés + objets de caisse

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Bien qu'aucun de ces éléments ne réponde vraiment à tout ce qu'une énumération java vous offre, voici les avantages et les inconvénients:

Énumération Scala

Avantages: -Fonctions pour instancier avec option ou supposant directement précis (plus facile lors du chargement à partir d'un magasin persistant) -Itération sur toutes les valeurs possibles est prise en charge

Inconvénients: l'avertissement de compilation pour la recherche non exhaustive n'est pas pris en charge (rend la correspondance des modèles moins idéale)

Objets / traits scellés

Avantages: -En utilisant des traits scellés, nous pouvons pré-instancier certaines valeurs tandis que d'autres peuvent être injectées au moment de la création

Inconvénients: -Instanciation à partir d'un magasin persistant - vous devez souvent utiliser la correspondance de modèles ici ou définir votre propre liste de toutes les "valeurs d'énumération" possibles

Ce qui m'a finalement fait changer d'avis était quelque chose comme l'extrait suivant:

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
}

Les .getappels étaient hideux - en utilisant l'énumération à la place, je peux simplement appeler la méthode withName sur l'énumération comme suit:

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

Je pense donc que ma préférence pour l'avenir est d'utiliser les énumérations lorsque les valeurs sont destinées à être accessibles à partir d'un référentiel et les objets / traits scellés dans le cas contraire.


Je peux voir comment le deuxième modèle de code est souhaitable (se débarrasser des deux méthodes d'aide du premier modèle de code). Cependant, j'ai trouvé une manière telle que vous n'êtes pas obligé de choisir entre ces deux modèles. Je couvre tout le domaine dans la réponse que j'ai publiée sur ce sujet: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

2

je préfère case objects (c'est une question de préférence personnelle). Pour faire face aux problèmes inhérents à cette approche (analyser la chaîne et itérer sur tous les éléments), j'ai ajouté quelques lignes qui ne sont pas parfaites, mais qui sont efficaces.

Je vous colle le code ici en espérant qu'il pourrait être utile, et que d'autres pourraient l'améliorer.

/**
 * 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 }
}

0

Pour ceux qui cherchent encore à faire fonctionner la réponse de GatesDa : Vous pouvez simplement référencer l'objet case après l'avoir déclaré pour l'instancier:

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

Je pense que le plus grand avantage d'avoir case classesplus enumerationsest que vous pouvez utiliser le modèle de classe de type aka polymorphysme ad hoc . Pas besoin de faire correspondre des énumérations comme:

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

à la place, vous aurez quelque chose comme:

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() = ...
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.