web-dev-qa-db-fra.com

Objets de cas et énumérations dans Scala

Existe-t-il des directives de meilleures pratiques sur le moment d'utiliser les classes de cas (ou les objets de cas) et d'étendre l'énumération dans Scala?

Ils semblent offrir certains des mêmes avantages.

215
Alex Miller

Une grande différence est que Enumerations est pris en charge pour les instancier à partir de name String. Par exemple:

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

Ensuite, vous pouvez faire:

val ccy = Currency.withName("EUR")

Ceci est utile si 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 donnent l'impression d'un ajout compliqué, alors j'ai tendance à utiliser maintenant case objects. Un case object est plus flexible qu'un enum:

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 @ chaotic3quilibrium a souligné (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 de chaîne de code de devise que "casser" la nature d'ensemble fermé du type Currency. UnknownCurrency étant de type Currency peut maintenant se faufiler dans d'autres parties d'une API.

Il est conseillé de repousser ce cas en dehors de la variable Enumeration et d'obliger le client à utiliser un type Option[Currency] qui indiquerait clairement qu'il existe réellement un problème de correspondance et d'encourager l'utilisateur de l'API à le résoudre lui-même.

Pour faire suite aux autres réponses ici, les principaux inconvénients de case objects sur Enumerations sont les suivants:

  1. Impossible de parcourir 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 de la valeur persistante. Ceci est également vrai, mais, sauf dans le cas d'énumérations énormes (par exemple, toutes les devises), cela ne représente pas une surcharge considérable.

213
oxbow_lakes

Les objets case renvoient déjà leur nom pour leurs méthodes toString, il est donc inutile de les transmettre séparément. Voici une version similaire à jho (méthodes de commodité omises pour des raisons de brièveté):

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 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 vous ne craignez pas de tricher, vous pouvez pré-charger vos valeurs d'énumération à l'aide de l'API de réflexion ou de quelque chose comme Google Reflections. Les objets de cas non paresseux vous donnent la syntaxe la plus propre:

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
}

Nice and clean, 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
62
GatesDA

L'utilisation des classes de cas par rapport aux énumérations présente les avantages 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 reprises dans la déclaration de correspondance. Avec les énumérations, le compilateur Scala ne peut pas dire.
  • Les classes de cas supportent naturellement plus de champs qu'une énumération basée sur la valeur qui prend en charge un nom et un ID.

L'utilisation des énumérations au lieu des classes de cas présente les avantages suivants:

  • Les énumérations seront généralement un peu moins de code à écrire.
  • Les énumérations sont un peu plus faciles à comprendre pour les débutants en 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 souhaitez que le compilateur soit plus sûr en vous indiquant si toutes les correspondances sont spécifiées, utilisez les classes de cas.

26
Aaron

UPDATE: Le code ci-dessous a un bogue, décrit ici . Le programme de test ci-dessous fonctionne, mais si vous utilisiez DayOfWeek.Mon (par exemple) avant DayOfWeek lui-même, cela é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, en forçant l'initialisation de vos énumérations, ou si vous pouvez utiliser les modifications de chaotic3quilibrium. Dans l'attente d'un enum basé sur les macros!


Si tu veux

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

alors ce qui suit peut être d’intérêt. Commentaires bienvenus.

Dans cette implémentation, il existe des classes de base abstraites Enum et EnumVal, 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 enum (appelez sa méthode apply) pour la concrétiser. [Je souhaite que les objets intérieurs ne soient pas paresseux, sauf si je le leur demande spécifiquement. Je pense.]

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

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 de jour" par "match (jour: @unchecked)" lorsque vous ne voulez pas de tels avertissements, ou simplement inclure un casier à 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 interrompre 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 identifiants et ajoute des données/méthodes à l'abstraction de Val et à l'énumération 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)
}
15
AmigoNico

J'ai ici une bibliothèque simple et agréable qui vous permet d'utiliser des traits/classes scellés comme valeurs enum sans avoir à gérer votre propre liste de valeurs. Il repose sur une macro simple qui ne dépend pas du buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum

11
lloydmeta

Mise à jour de mars 2017: commenté par Anthony Accioly , le scala.Enumeration/enum PR a été fermé.

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


Remarque: il existe maintenant (août 2016, 6 ans et plus plus tard) une proposition visant à supprimer scala.Enumeration: PR 5352

Obsolète scala.Enumeration, ajoutez l'annotation @enum

La syntaxe

@enum
 class Toggle {
  ON
  OFF
 }

est un exemple d'implémentation possible, l'intention est de prendre également en charge les ADT conformes à certaines restrictions (absence d'imbrication, de récursivité ou de modification des paramètres du constructeur), e. g.:

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

Observe le désastre non résolu qui est scala.Enumeration.

Avantages de @enum sur scala.Enumeration:

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

Inconvénients: aucun.

Cela résout le problème de l’impossibilité d’avoir une base de code qui prend en charge Scala-JVM, Scala.js et Scala-Native (le code source Java n'est pas pris en charge sur Scala.js/Scala-Native, le code source Scala ne permet pas de définir les énumérations acceptées par les API existantes sur Scala-JVM).

10
VonC

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

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

8
user142435

Si vous tenez vraiment au maintien de l'interopérabilité avec d'autres langages JVM (par exemple, Java), la meilleure option consiste à écrire des énumérations Java. Ceux-ci fonctionnent de manière transparente à partir de code Scala et Java, ce qui est plus que ne peut être dit pour les objets scala.Enumeration ou case. N'ayons pas de nouvelle bibliothèque d'énumérations pour chaque nouveau projet de loisir sur GitHub, si cela peut être évité!

5
Connor Doyle

J'ai vu diverses versions d'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 proposer un meilleur truc que d'ajouter simplement une classe de cas à la liste comme je l'ai fait. C’était tout ce que je pouvais trouver à l’époque.

4
jho

Je suis allé et retour 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 de trait/objet de 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 cas

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Bien qu'aucune de celles-ci ne réponde vraiment à tout ce qu'une énumération de Java vous donne, voici les avantages et les inconvénients:

Enumération Scala

Avantages: - Fonctions pour instancier avec option ou supposer directement exactes (plus facile lors du chargement depuis un magasin persistant) - L'itération sur toutes les valeurs possibles est prise en charge 

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

Objets de cas/traits scellés

Avantages: - À l'aide de traits scellés, nous pouvons pré-instancier certaines valeurs, tandis que d'autres peuvent être injectées au moment de la création - Prise en charge complète de la correspondance de modèle (méthodes apply/unapply définies)

Inconvénients: - Instanciation depuis un magasin persistant - vous devez souvent utiliser la correspondance de modèle ici ou définir votre propre liste de toutes les «valeurs énumérées» possibles

Ce qui m'a finalement amené à 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 appels .get é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 des énumérations lorsque les valeurs doivent être accessibles à partir d’un référentiel et que les objets de cas/traits scellés sont sinon utilisés.

2
Mad Dog

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 (chaîne d'analyse et itération de 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 }
}
2
jamming

Je pense que le plus grand avantage d’avoir case classes sur enumerations est que vous pouvez utiliser le modèle de classe a.k.a ad-hoc polymorphysm. Vous n'avez pas besoin de faire correspondre des enums 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() = ...
}
0
Murat Mustafin

Pour ceux qui cherchent encore comment obtenir la réponse de GatesDa au travail : 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
V-Lamp