Oggetti caso vs enumerazioni in Scala


231

Esistono linee guida sulle migliori pratiche su quando utilizzare le classi case (o gli oggetti case) rispetto all'estensione dell'enumerazione in Scala?

Sembrano offrire alcuni degli stessi vantaggi.


2
Ho scritto una piccola panoramica su Scala Enumeration e alternative, potresti trovarlo utile: pedrorijo.com/blog/scala-enums/
pedrorijo91

1
Vedi anche la Scala 3 basata su Dottyenum (per metà 2020).
VonC,

Risposte:


223

Una grande differenza è che viene Enumerationfornito con il supporto per istanziarli da qualche namestringa. Per esempio:

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

Quindi puoi fare:

val ccy = Currency.withName("EUR")

Ciò è utile quando si desidera conservare le enumerazioni (ad esempio in un database) o crearle dai dati che risiedono nei file. Tuttavia, trovo in generale che le enumerazioni siano un po 'goffe in Scala e abbiano la sensazione di un componente aggiuntivo imbarazzante, quindi ora tendo a usare case objects. A case objectè più flessibile di un enum:

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

case class UnknownCurrency(name: String) extends Currency

Quindi ora ho il vantaggio di ...

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

Come ha sottolineato @ chaotic3quilibrium (con alcune correzioni per facilitare la lettura):

Per quanto riguarda il modello "UnknownCurrency (code)", esistono altri modi per gestire il non trovare una stringa di codice di valuta oltre a "spezzare" il tipo di set chiuso del Currencytipo. UnknownCurrencyessere di tipo Currencyora può intrufolarsi in altre parti di un'API.

È consigliabile spingere quel caso all'esterno Enumeratione fare in modo che il client gestisca un Option[Currency]tipo che indichi chiaramente che esiste davvero un problema di corrispondenza e "incoraggia" l'utente dell'API a risolverlo da solo.

Per dare seguito alle altre risposte qui, i principali svantaggi di case objects su Enumerations sono:

  1. Impossibile iterare su tutte le istanze dell '"enumerazione" . Questo è certamente il caso, ma ho trovato estremamente raro in pratica che ciò sia necessario.

  2. Impossibile creare un'istanza facilmente dal valore persistente . Questo è anche vero ma, tranne nel caso di enormi enumerazioni (ad esempio, tutte le valute), ciò non presenta un enorme sovraccarico.


10
L'altra differenza è che l'enumerazione dell'enumerazione è ordinata per impostazione predefinita, mentre l'enum basato sull'oggetto case ovviamente non è
om-nom-nom

1
Un altro punto per gli oggetti case è se ti interessa l'interoperabilità di Java. L'enumerazione restituirebbe i valori come Enumeration.Value, quindi 1) richiedendo scala-library, 2) perdendo le informazioni sul tipo effettivo.
juanmirocks,

7
@oxbow_lakes Per quanto riguarda il punto 1, in particolare questa parte "... Ho trovato estremamente raro in pratica che ciò sia necessario": Apparentemente raramente fai molto lavoro sull'interfaccia utente. Questo è un caso d'uso estremamente comune; visualizzazione di un elenco (a discesa) di membri di enumerazione validi da cui selezionare.
chaotic3quilibrium,

Non capisco il tipo di oggetto da abbinare trade.ccynell'esempio tratto sigillato.
rloth

e non case objectgenerano un ingombro di codice maggiore (~ 4x) di Enumeration? Distinzione utile soprattutto per i scala.jsprogetti che richiedono un ingombro ridotto.
ecoe,

69

AGGIORNAMENTO: è stata creata una nuova soluzione basata su macro che è di gran lunga superiore alla soluzione che ho delineato di seguito. Consiglio vivamente di utilizzare questa nuova soluzione basata su macro . E sembra che i piani per Dotty rendano questo stile di soluzione enum parte del linguaggio. Whoohoo!

Riepilogo:
Esistono tre schemi di base per tentare di riprodurre Java Enumall'interno di un progetto Scala. Due dei tre modelli; usando direttamente Java Enume scala.Enumeration, non sono in grado di abilitare l'esaustivo pattern matching di Scala. E il terzo; "tratto sigillato + oggetto case", ha ... ma presenta complicazioni di inizializzazione della classe / oggetto JVM che generano una generazione di indice ordinale incoerente.

Ho creato una soluzione con due classi; Enumerazione ed enumerazioneDecorated , situato in questo Gist . Non ho inserito il codice in questa discussione poiché il file per l'Enumerazione era piuttosto grande (+400 righe - contiene molti commenti che spiegano il contesto di implementazione).

Dettagli:
la domanda che stai ponendo è piuttosto generale; "... quando usare le caseclassiobjects contro l'estensione [scala.]Enumeration". E si scopre che ci sono MOLTE possibili risposte, ciascuna risposta a seconda delle sottigliezze dei requisiti specifici del progetto che hai. La risposta può essere ridotta a tre schemi di base.

Per iniziare, assicuriamoci di lavorare dalla stessa idea di base di cosa sia un'enumerazione. Definiamo un'enumerazione principalmente in termini di Enumforniti a partire da Java 5 (1.5) :

  1. Contiene una serie chiusa ordinata di membri denominati
    1. C'è un numero fisso di membri
    2. I membri vengono naturalmente ordinati e indicizzati esplicitamente
      • Invece di essere ordinati in base ad alcuni criteri interrogabili dei membri inate
    3. Ogni membro ha un nome univoco all'interno dell'insieme totale di tutti i membri
  2. Tutti i membri possono essere facilmente ripetuti in base ai loro indici
  3. Un membro può essere recuperato con il suo nome (sensibile al maiuscolo / minuscolo)
    1. Sarebbe abbastanza bello se un membro potesse anche essere recuperato con il suo nome senza distinzione tra maiuscole e minuscole
  4. Un membro può essere recuperato con il suo indice
  5. I membri possono utilizzare la serializzazione in modo semplice, trasparente ed efficiente
  6. I membri possono essere facilmente estesi per contenere ulteriori dati di singolarità associati
  7. Pensando oltre Java Enum, sarebbe bello essere in grado di sfruttare esplicitamente il modello di Scala che combina l'esaustività controllando un'enumerazione

Quindi, diamo un'occhiata alle versioni ridotte dei tre modelli di soluzione più comuni pubblicati:

A) In realtà direttamente usando ilEnum pattern Java (in un progetto misto 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;
  }
}

I seguenti elementi dalla definizione dell'enumerazione non sono disponibili:

  1. 3.1 - Sarebbe abbastanza bello se un membro potesse anche essere recuperato con il suo nome senza distinzione tra maiuscole e minuscole
  2. 7 - Pensando al di là di Enum di Java, sarebbe bello poter sfruttare esplicitamente il pattern di Scala abbinando l'esaustività controllando un elenco

Per i miei progetti attuali, non ho il vantaggio di assumermi i rischi legati al percorso dei progetti misti Scala / Java. E anche se potessi scegliere di fare un progetto misto, l'elemento 7 è fondamentale per consentirmi di rilevare problemi di tempo di compilazione se / quando aggiungo / rimuovo membri di enumerazione o sto scrivendo un nuovo codice per gestire i membri di enumerazione esistenti.


B) Utilizzando il modello " 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}
}

I seguenti elementi dalla definizione dell'enumerazione non sono disponibili:

  1. 1.2 - I membri sono naturalmente ordinati e indicizzati esplicitamente
  2. 2 - Tutti i membri possono essere ripetuti facilmente in base ai loro indici
  3. 3 - Un membro può essere recuperato con il suo nome (sensibile al maiuscolo / minuscolo)
  4. 3.1 - Sarebbe abbastanza bello se un membro potesse anche essere recuperato con il suo nome senza distinzione tra maiuscole e minuscole
  5. 4 - Un membro può essere recuperato con il suo indice

È discutibile che soddisfi davvero gli elementi di definizione dell'enumerazione 5 e 6. Per 5, è un tratto affermare che sia efficiente. Per 6, non è davvero facile estenderlo per contenere ulteriori dati di singolarità associati.


C) Utilizzo del scala.Enumerationmodello (ispirato a questa risposta 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]
}

I seguenti elementi dalla definizione dell'enumerazione non sono disponibili (sembra essere identico all'elenco per l'utilizzo diretto di Java Enum):

  1. 3.1 - Sarebbe abbastanza bello se un membro potesse anche essere recuperato con il suo nome senza distinzione tra maiuscole e minuscole
  2. 7 - Pensando al di là di Enum di Java, sarebbe bello poter sfruttare esplicitamente il pattern di Scala abbinando l'esaustività controllando un elenco

Ancora una volta per i miei progetti attuali, l'articolo 7 è fondamentale per consentirmi di rilevare i problemi di compilazione se / quando aggiungo / rimuovo i membri dell'enumerazione o sto scrivendo un nuovo codice per gestire i membri dell'enumerazione esistenti.


Pertanto, data la definizione di enumerazione sopra riportata, nessuna delle tre soluzioni precedenti funziona poiché non forniscono tutto quanto indicato nella definizione di enumerazione sopra:

  1. Java Enum direttamente in un progetto misto Scala / Java
  2. "tratto sigillato + oggetti custodia"
  3. scala.Enumeration

Ognuna di queste soluzioni può essere eventualmente rielaborata / ampliata / refactored per tentare di coprire alcuni dei requisiti mancanti di ognuno. Tuttavia, né Java Enumné le scala.Enumerationsoluzioni possono essere sufficientemente espanse per fornire il punto 7. E per i miei progetti, questo è uno dei valori più convincenti dell'uso di un tipo chiuso all'interno di Scala. Preferisco fortemente avvisi / errori in fase di compilazione per indicare che ho un gap / problema nel mio codice piuttosto che doverlo eliminare da un'eccezione / errore di runtime di produzione.


A tale proposito, ho iniziato a lavorare con il case objectpercorso per vedere se potevo produrre una soluzione che coprisse tutta la definizione di enumerazione sopra. La prima sfida è stata quella di superare il nucleo del problema di inizializzazione della classe / oggetto JVM (trattato in dettaglio in questo post StackOverflow ). E sono stato finalmente in grado di trovare una soluzione.

Poiché la mia soluzione è di due tratti; Enumerazione ed enumerazioneDecorated , e poiché il Enumerationtratto è lungo +400 righe (molti commenti che spiegano il contesto), sto rinunciando a incollarlo in questo thread (che lo farebbe allungare considerevolmente lungo la pagina). Per i dettagli, vai direttamente al Gist .

Ecco come appare la soluzione utilizzando la stessa idea di dati di cui sopra (versione completamente commentata disponibile qui ) e implementata in 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
}

Questo è un esempio di utilizzo di una nuova coppia di tratti di enumerazione che ho creato (situato in questo Gist ) per implementare tutte le capacità desiderate e delineate nella definizione di enumerazione.

Una preoccupazione espressa è che i nomi dei membri dell'enumerazione devono essere ripetuti ( decorationOrderedSetnell'esempio sopra). Mentre l'ho minimizzato fino a una singola ripetizione, non riuscivo a vedere come renderlo ancora meno a causa di due problemi:

  1. L'inizializzazione dell'oggetto / classe JVM per questo particolare modello oggetto / caso non è definita (vedere questo thread StackOverflow )
  2. Il contenuto restituito dal metodo getClass.getDeclaredClassesha un ordine indefinito (ed è abbastanza improbabile che si trovi nello stesso ordine delle case objectdichiarazioni nel codice sorgente)

Alla luce di questi due problemi, ho dovuto rinunciare a cercare di generare un ordine implicito e ho dovuto richiedere esplicitamente al cliente di definirlo e dichiararlo con una sorta di nozione ordinata. Dato che le raccolte Scala non hanno un'implementazione dell'insieme ordinato inserto, la cosa migliore che potevo fare era usare un Liste quindi verificare il runtime che fosse veramente un insieme. Non è come avrei preferito averlo raggiunto.

E dato il design richiesto questo secondo elenco / set ordinazione val, dato l' ChessPiecesEnhancedDecoratedesempio di cui sopra, è stato possibile aggiungere case object PAWN2 extends Membere poi dimenticare di aggiungere Decoration(PAWN2,'P2', 2)a decorationOrderedSet. Quindi, c'è un controllo di runtime per verificare che l'elenco non sia solo un set, ma contenga TUTTI gli oggetti case che estendono il sealed trait Member. Era una forma speciale di riflessione / macro inferno da elaborare.


Si prega di lasciare commenti e / o feedback sul Gist .


Ora ho rilasciato la prima versione della libreria ScalaOlio (GPLv3) che contiene versioni più aggiornate di entrambi org.scalaolio.util.Enumeratione org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium

E per saltare direttamente al repository ScalaOlio su Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium

5
Questa è una risposta di qualità e molto da trarne. Grazie
angabriel,

1
Sembra che Odersky voglia aggiornare Dotty (la futura Scala 3.0) con un enum nativo. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium

62

Gli oggetti Case restituiscono già il loro nome per i loro metodi toString, quindi non è necessario passarlo separatamente. Ecco una versione simile a quella di jho (metodi di convenienza omessi per brevità):

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

Gli oggetti sono pigri; usando vals invece possiamo eliminare la lista ma dobbiamo ripetere il nome:

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

Se non ti dispiace un po 'di imbrogli, puoi precaricare i valori di enumerazione usando l'API reflection o qualcosa come Google Reflections. Gli oggetti case non pigri offrono la sintassi più pulita:

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
}

Bello e pulito, con tutti i vantaggi di classi di casi ed enumerazioni Java. Personalmente, definisco i valori di enumerazione all'esterno dell'oggetto per abbinare meglio il codice Scala idiomatico:

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

3
una domanda: l'ultima soluzione si chiama "oggetti non pigri" ma in questo caso gli oggetti non vengono caricati fino a quando non li usiamo: perché questa soluzione non è pigra?
Seb Cesbron,

2
@Noel, è necessario utilizzare: incolla per incollare l'intera gerarchia sigillata nella REPL. In caso contrario, la riga singola con la classe / tratto di base sigillata viene conteggiata come un singolo file, viene sigillata immediatamente e non può essere estesa sulla riga successiva.
Jürgen Strobel,

2
@GatesDA Solo il tuo primo frammento di codice non ha un bug (poiché richiedi esplicitamente al client di dichiarare e definire i valori. Sia la tua seconda che la terza soluzione hanno il bug sottile che ho descritto nel mio ultimo commento (se il client accede a Currency .GBP direttamente e prima di tutto, l'elenco dei valori sarà "fuori servizio"). Ho esplorato ampiamente il dominio di enumerazione Scala e l'ho coperto in dettaglio nella mia risposta a questo stesso thread: stackoverflow.com/a/25923651/501113
chaotic3quilibrium,

1
Forse uno degli svantaggi di questo approccio (rispetto comunque a Java Enums) è che quando si digita Currency <dot> in IDE non mostra le opzioni disponibili.
Ivan Balashov

1
Come menzionato @SebCesbron, qui gli oggetti case sono pigri. Quindi, se chiamo Currency.values, ottengo solo valori indietro a cui ho avuto accesso in precedenza. C'è un modo per aggirare questo?
Sasgorilla,

27

I vantaggi dell'utilizzo delle classi di casi rispetto alle enumerazioni sono:

  • Quando si usano classi di maiuscole, il compilatore Scala può dire se la corrispondenza è specificata per intero, ad es. Quando tutte le possibili corrispondenze sono sposate nella dichiarazione di corrispondenza. Con le enumerazioni, il compilatore Scala non può dirlo.
  • Le classi di casi supportano naturalmente più campi di un'enumerazione basata su valori che supporta un nome e un ID.

I vantaggi dell'utilizzo delle enumerazioni anziché delle classi di casi sono:

  • Le enumerazioni saranno generalmente un po 'meno di codice da scrivere.
  • Le enumerazioni sono un po 'più facili da capire per chi non conosce Scala poiché sono prevalenti in altre lingue

Quindi, in generale, se hai solo bisogno di un elenco di costanti semplici per nome, usa le enumerazioni. Altrimenti, se hai bisogno di qualcosa di un po 'più complesso o desideri che la sicurezza aggiuntiva del compilatore ti dica se sono state specificate tutte le corrispondenze, usa le classi case.


15

AGGIORNAMENTO: Il codice seguente ha un bug, descritto qui . Il seguente programma di test funziona, ma se dovessi usare DayOfWeek.Mon (ad esempio) prima di DayOfWeek stesso, fallirebbe perché DayOfWeek non è stato inizializzato (l'uso di un oggetto interno non provoca l'inizializzazione di un oggetto esterno). Puoi ancora usare questo codice se fai qualcosa come val enums = Seq( DayOfWeek )nella tua classe principale, forzando l'inizializzazione dei tuoi enum o puoi usare le modifiche di chaotic3quilibrium. In attesa di un enum basato su macro!


Se vuoi

  • avvertenze su corrispondenze di pattern non esaustivi
  • un ID Int assegnato a ciascun valore enum, che è possibile controllare facoltativamente
  • un elenco immutabile dei valori enum, nell'ordine in cui sono stati definiti
  • una mappa immutabile dal nome al valore enum
  • una mappa immutabile dall'id al valore enum
  • luoghi in cui applicare metodi / dati per tutti o particolari valori enum o per l'enum nel suo insieme
  • valori enum ordinati (in modo da poter verificare, ad esempio, se il giorno <mercoledì)
  • la capacità di estendere un enum per crearne altri

quindi potrebbe essere interessante il seguente. Feedback di benvenuto.

In questa implementazione ci sono classi base Enum ed EnumVal astratte, che estendi. Vedremo quelle lezioni tra un minuto, ma prima, ecco come definiresti un enum:

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

Si noti che è necessario utilizzare ciascun valore enum (chiamare il suo metodo apply) per dargli vita. [Vorrei che gli oggetti interiori non fossero pigri a meno che non glielo chiedessi espressamente. Penso.]

Naturalmente potremmo aggiungere metodi / dati a DayOfWeek, Val o ai singoli oggetti caso se lo desiderassimo.

Ed ecco come useresti un tale enum:

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

}

Ecco cosa ottieni quando lo compili:

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

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

Puoi sostituire la "corrispondenza del giorno" con la corrispondenza "(giorno: @unchecked)" dove non desideri tali avvisi o semplicemente includere un caso generale alla fine.

Quando si esegue il programma precedente, si ottiene questo output:

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

Si noti che poiché l'elenco e le mappe sono immutabili, è possibile rimuovere facilmente gli elementi per creare sottoinsiemi, senza interrompere l'enum stesso.

Ecco la stessa classe Enum (e EnumVal al suo interno):

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

}

Ed ecco un suo uso più avanzato che controlla gli ID e aggiunge dati / metodi all'astrazione di Val e all'enum stesso:

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 per averlo fornito. Lo apprezzo molto. Tuttavia, sto notando che sta usando "var" al contrario di val. E questo è un peccato mortale limite nel mondo FP. Quindi, c'è un modo per implementarlo in modo tale che non vi sia alcun uso di var? Sono solo curioso di sapere se si tratta di una sorta di caso limite di tipo FP e non capisco come la tua implementazione sia indesiderabile.
chaotic3quilibrium,

2
Probabilmente non posso aiutarti. È abbastanza comune in Scala scrivere classi che mutano internamente ma che sono immutabili per coloro che le usano. Nell'esempio sopra, un utente di DayOfWeek non può mutare l'enum; non è possibile, ad esempio, modificare l'ID di martedì o il suo nome dopo il fatto. Ma se vuoi un'implementazione priva di mutazione internamente , allora non ho nulla. Non sarei sorpreso, tuttavia, di vedere una nuova funzione enum basata su macro in 2.11; le idee vengono prese a calci su scala-lang.
AmigoNico,

Ricevo uno strano errore nel foglio di lavoro Scala. Se utilizzo direttamente una delle istanze Value, visualizzo un errore di inizializzazione. Tuttavia, se faccio una chiamata al metodo .values ​​per vedere il contenuto dell'enumerazione, funziona e quindi utilizza direttamente l'istanza del valore. Qualche idea su quale sia l'errore di inizializzazione? E qual è il modo ottimale per garantire che l'inizializzazione avvenga nell'ordine corretto indipendentemente dalla convenzione di chiamata?
chaotic3quilibrium,

@ chaotic3quilibrium: Wow! Grazie per aver perseguito questo obiettivo e, naturalmente, grazie a Rex Kerr per il sollevamento pesante. Citerò qui il problema e farò riferimento alla domanda che hai creato.
AmigoNico,

"[L'uso var] è un peccato mortale limite nel mondo del PQ": non credo che l'opinione sia universalmente accettata.
Erik Kaplun,

12

Ho una bella lib semplice qui che ti permette di usare tratti / classi sigillati come valori enum senza dover mantenere il tuo elenco di valori. Si basa su una semplice macro che non dipende dal buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum


10

Aggiornamento marzo 2017: come commentato da Anthony Accioly , il scala.Enumeration/enumPR è stato chiuso.

Dotty (compilatore di prossima generazione per Scala) prenderà il comando, anche se il numero dotty del 1970 e il PR 1958 di Martin Odersky .


Nota: ora c'è (agosto 2016, 6+ anni dopo) una proposta da rimuovere scala.Enumeration: PR 5352

Elimina scala.Enumeration, aggiungi @enumannotazione

La sintassi

@enum
 class Toggle {
  ON
  OFF
 }

è un possibile esempio di implementazione, l'intenzione è di supportare anche ADT conformi a determinate restrizioni (nessun annidamento, ricorsione o parametri variabili del costruttore), ad esempio:

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

Depreca il disastro non mitigato che è scala.Enumeration.

Vantaggi di @enum rispetto a scala.Enumeration:

  • Funziona davvero
  • Interoperabilità Java
  • Nessun problema di cancellazione
  • Nessun mini-DSL confuso da imparare quando si definiscono le enumerazioni

Svantaggi: nessuno.

Questo risolve il problema di non poter avere un codebase che supporti Scala-JVM Scala.jse Scala-Native (codice sorgente Java non supportato su Scala.js/Scala-Native, codice sorgente Scala non in grado di definire enumerazioni accettate dalle API esistenti su Scala-JVM).


Il PR sopra ha chiuso (nessuna gioia). Ora è il 2017 e sembra che Dotty avrà finalmente un costrutto enum. Ecco il problema e le PR di Martin . Unisci, unisci, unisci!
Anthony Accioly

8

Un altro svantaggio delle classi di casi rispetto alle enumerazioni quando è necessario iterare o filtrare in tutte le istanze. Questa è una funzionalità integrata di enumerazione (e anche di enumerazioni Java) mentre le classi di casi non supportano automaticamente tale funzionalità.

In altre parole: "non esiste un modo semplice per ottenere un elenco dell'insieme totale di valori enumerati con le classi case".


5

Se sei seriamente intenzionato a mantenere l'interoperabilità con altri linguaggi JVM (es. Java), l'opzione migliore è scrivere enum Java. Funzionano in modo trasparente sia dal codice Scala che Java, che è più di quanto si possa dire per scala.Enumerationo case case. Non abbiamo una nuova libreria di enumerazioni per ogni nuovo progetto di hobby su GitHub, se può essere evitato!


4

Ho visto varie versioni del fare in modo che una classe di casi imiti un'enumerazione. Ecco la mia versione:

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

Ciò consente di creare classi di casi simili alle seguenti:

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

Forse qualcuno potrebbe escogitare un trucco migliore che semplicemente aggiungere una classe di ciascun caso all'elenco come ho fatto io. Questo era tutto ciò che potevo inventare in quel momento.


Perché due metodi non applicabili separati?
Saish,

@jho Ho cercato di elaborare la tua soluzione così com'è, ma non verrà compilata. Nel secondo frammento di codice, c'è un riferimento a Sito in "tipo V = Sito". Non sono sicuro di cosa si riferisca per eliminare l'errore di compilazione. Quindi, perché stai fornendo le parentesi graffe vuote per "valuta di classe astratta"? Non potevano essere lasciati fuori? Infine, perché stai usando una var in "var valori = ..."? Questo non significa che i clienti potrebbero in qualsiasi momento e da qualsiasi parte del codice assegnare un nuovo Elenco ai valori? Non sarebbe di gran lunga preferibile renderlo un val anziché un var?
chaotic3quilibrium,

2

Sono andato avanti e indietro su queste due opzioni le ultime volte che ne ho avuto bisogno. Fino a poco tempo fa, la mia preferenza era per l'opzione trait / case object sigillata.

1) Dichiarazione di enumerazione Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Tratti sigillati + oggetti caso

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Mentre nessuno di questi soddisfa davvero tutto ciò che ti dà un elenco di Java, di seguito sono i pro e i contro:

Enumerazione Scala

Pro: -Funzioni per istanziare con opzione o assumere direttamente accurate (più facile quando si carica da un archivio persistente) -L'interazione su tutti i valori possibili è supportata

Contro: -L'avviso di compilazione per la ricerca non esaustiva non è supportato (rende la corrispondenza dei modelli meno ideale)

Oggetti caso / tratti sigillati

Pro: -Utilizzando tratti sigillati, possiamo pre-istanziare alcuni valori mentre altri possono essere iniettati al momento della creazione, supporto completo per la corrispondenza dei modelli (definiti metodi di applicazione / non applicazione)

Contro: -Istantanea da un negozio persistente - spesso devi usare la corrispondenza dei modelli qui o definire il tuo elenco di tutti i possibili "valori di enum"

Ciò che alla fine mi ha fatto cambiare la mia opinione è stato qualcosa come il seguente frammento:

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
}

Le .getchiamate erano orribili - usando l'enumerazione invece posso semplicemente chiamare il metodo withName sull'enumerazione come segue:

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

Quindi penso che la mia preferenza in futuro sia quella di usare le enumerazioni quando si intende accedere ai valori da un repository e oggetti case / tratti sigillati altrimenti.


Vedo come è desiderabile il secondo modello di codice (eliminando i due metodi di supporto dal primo modello di codice). Tuttavia, ho scoperto un modo in cui non sei costretto a scegliere tra questi due schemi. I coprire l'intero dominio nella risposta che ho inviato a questo thread: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

2

Preferisco case objects(è una questione di preferenze personali). Per far fronte ai problemi inerenti a quell'approccio (analizzare la stringa e iterare su tutti gli elementi), ho aggiunto alcune righe che non sono perfette, ma sono efficaci.

Ti sto incollando il codice qui aspettandomi che possa essere utile e anche che altri possano migliorarlo.

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

Per coloro che stanno ancora cercando come far funzionare la risposta di GatesDa : puoi semplicemente fare riferimento all'oggetto case dopo averlo dichiarato per istanziarlo:

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

Penso che il più grande vantaggio di avere case classesoltre enumerationssia che puoi usare il modello di classe di tipo aka polimorfismo ad hoc . Non è necessario abbinare enum come:

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

invece avrai qualcosa del tipo:

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() = ...
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.