Come modellare tipi enum sicuri?


311

Scala non ha messaggi di sicurezza enumcome Java. Dato un insieme di costanti correlate, quale sarebbe il modo migliore in Scala di rappresentare quelle costanti?


2
Perché non usare solo java enum? Questa è una delle poche cose che preferisco ancora usare java semplice.
Max

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

Risposte:


187

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

Esempio di utilizzo

  object Main extends App {

    object WeekDay extends Enumeration {
      type WeekDay = Value
      val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
    }
    import WeekDay._

    def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

    WeekDay.values filter isWorkingDay foreach println
  }

2
Scherzi a parte, l'applicazione non deve essere utilizzata. NON è stato risolto; è stata introdotta una nuova classe, App, che non presenta i problemi menzionati da Schildmeijer. Quindi fai "object foo extends App {...}" E hai accesso immediato agli argomenti della riga di comando attraverso la variabile args.
AmigoNico,

scala.Enumeration (che è quello che stai usando nel tuo esempio di codice "oggetto WeekDay" sopra) non offre una corrispondenza esaustiva del modello. Ho studiato tutti i diversi schemi di enumerazione attualmente utilizzati in Scala e ne ho dato e una panoramica in questa risposta StackOverflow (incluso un nuovo schema che offre il meglio sia di scala.Enumeration sia del modello "trait + + case object": stackoverflow. com / a / 25923651/501113
chaotic3quilibrium,

377

Devo dire che l'esempio copiato dalla documentazione Scala di skaffman sopra è di utilità limitata in pratica (potresti anche usare case objects).

Per ottenere qualcosa che assomigli di più a un Java Enum(cioè con metodi toStringe valueOfmetodi sensibili , forse stai persistendo i valori enum in un database) devi modificarlo un po '. Se avessi usato il codice di skaffman :

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

Considerando che usando la seguente dichiarazione:

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

Ottieni risultati più sensati:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue

7
Btw. Il metodo valueOf ora è morto :-(
greenoldman,

36
valueOfLa sostituzione di @macias è withNameche non restituisce un'opzione e genera un NSE se non c'è corrispondenza. Che cosa!
Bluu,

6
@Bluu Puoi aggiungere valore di te stesso: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) per avere un'opzione
centr

@centr Quando provo a creare un Map[Weekday.Weekday, Long]e aggiungere un valore ditegli Monche il compilatore genera un errore di tipo non valido. Weekday atteso. Week-end trovato valore? Perché succede?
Sohaib,

@Sohaib Dovrebbe essere Map [Weekday.Value, Long].
centr

99

Esistono molti modi per farlo.

1) Usa i simboli. Tuttavia, non ti darà alcun tipo di sicurezza, oltre a non accettare non-simboli in cui è previsto un simbolo. Sto solo citandolo qui per completezza. Ecco un esempio di utilizzo:

def update(what: Symbol, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case 'row => replaceRow(where, newValue)
    case 'col | 'column => replaceCol(where, newValue)
    case _ => throw new IllegalArgumentException
  }

// At REPL:   
scala> val a = unitMatrixInt(3)
a: teste7.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a('row, 1) = a.row(0)
res41: teste7.MatrixInt =
/ 1 0 0 \
| 1 0 0 |
\ 0 0 1 /

scala> a('column, 2) = a.row(0)
res42: teste7.MatrixInt =
/ 1 0 1 \
| 0 1 0 |
\ 0 0 0 /

2) Utilizzando la classe Enumeration:

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

oppure, se è necessario serializzare o visualizzarlo:

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

Questo può essere usato in questo modo:

def update(what: Dimension, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case Row => replaceRow(where, newValue)
    case Column => replaceCol(where, newValue)
  }

// At REPL:
scala> a(Row, 2) = a.row(1)
<console>:13: error: not found: value Row
       a(Row, 2) = a.row(1)
         ^

scala> a(Dimension.Row, 2) = a.row(1)
res1: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

scala> import Dimension._
import Dimension._

scala> a(Row, 2) = a.row(1)
res2: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

Sfortunatamente, non garantisce che tutte le partite vengano prese in considerazione. Se avessi dimenticato di mettere Row o Column nella partita, il compilatore Scala non mi avrebbe avvertito. Quindi mi dà un po 'di sicurezza, ma non quanto si può guadagnare.

3) Oggetti del caso:

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

Ora, se lascio un caso su un match, il compilatore mi avvertirà:

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

    what match {
    ^
one warning found

È usato praticamente allo stesso modo e non ha nemmeno bisogno di un import:

scala> val a = unitMatrixInt(3)
a: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a(Row,2) = a.row(0)
res15: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 1 0 0 /

Potresti chiederti, quindi, perché mai usare un'enumerazione invece degli oggetti case. È un dato di fatto, gli oggetti caso presentano vantaggi molte volte, come qui. La classe Enumeration, tuttavia, ha molti metodi Collection, come gli elementi (iteratore su Scala 2.8), che restituisce un Iteratore, mappa, flatMap, filtro, ecc.

Questa risposta è essenzialmente una parte selezionata di questo articolo nel mio blog.


"... non accettare non simboli in cui è previsto un simbolo"> Suppongo che tu intenda che le Symbolistanze non possono avere spazi o caratteri speciali. La maggior parte delle persone al primo incontro con la Symbolclasse probabilmente lo pensa, ma in realtà non è corretta. Symbol("foo !% bar -* baz")compila ed esegue perfettamente bene. In altre parole, puoi creare perfettamente Symbolistanze avvolgendo qualsiasi stringa (semplicemente non puoi farlo con lo zucchero sintattico "coma singolo"). L'unica cosa che Symbolgarantisce è l'unicità di ogni dato simbolo, che rende leggermente più veloce il confronto e il confronto.
Régis Jean-Gilles,

@ RégisJean-Gilles No, intendo dire che non puoi passare un Stringargomento, ad esempio, a un Symbolparametro.
Daniel C. Sobral,

Sì, ho capito quella parte, ma è un bel punto controverso se si sostituisce Stringcon un'altra classe che è sostanzialmente un involucro attorno a una stringa e può essere liberamente convertito in entrambe le direzioni (come nel caso di Symbol). Immagino sia quello che volevi dire dicendo "Non ti darà alcun tipo di sicurezza", semplicemente non era molto chiaro dato che OP ha chiesto esplicitamente soluzioni di tipo sicuro. Non ero sicuro se al momento della scrittura si sapeva che non solo non è digitare sicuro perché quelli non sono enumerazioni a tutti, ma anche Symbol s non hanno nemmeno la garanzia che l'argomento passato non avrà i caratteri speciali.
Régis Jean-Gilles,

1
Per elaborare, quando si dice "non accettare non simboli in cui è previsto un simbolo", può essere letto come "non accettare valori che non sono istanze di Simbolo" (che è ovviamente vero) o "non accettare valori che non sono identificatore simile pianura corde, alias 'simboli'"(che non è vero, ed è un equivoco che praticamente nessuno ha la prima volta che incontriamo simboli Scala, a causa del fatto che il primo incontro è però la speciale 'foonotazione che fa ostino stringhe non identificative). Questo è questo malinteso che volevo dissipare per qualsiasi lettore futuro.
Régis Jean-Gilles,

@ RégisJean-Gilles Intendevo il primo, quello che è ovviamente vero. Voglio dire, è ovviamente vero per chiunque sia abituato alla digitazione statica. All'epoca c'erano molte discussioni sui meriti relativi della tipizzazione statica e "dinamica", e molte persone interessate a Scala provenivano da un background di battitura dinamico, quindi ho pensato che non sarebbe stato inutile dirlo. Oggi non avrei nemmeno pensato di fare questa osservazione. Personalmente, penso che il simbolo di Scala sia brutto e ridondante e non lo usi mai. Sto votando il tuo ultimo commento, dal momento che è un buon punto.
Daniel C. Sobral,

52

Un modo leggermente meno prolisso per dichiarare le enumerazioni denominate:

object WeekDay extends Enumeration("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") {
  type WeekDay = Value
  val Sun, Mon, Tue, Wed, Thu, Fri, Sat = Value
}

WeekDay.valueOf("Wed") // returns Some(Wed)
WeekDay.Fri.toString   // returns Fri

Ovviamente il problema qui è che dovrai sincronizzare l'ordinamento dei nomi e dei valori che è più facile da fare se nome e valore sono dichiarati sulla stessa riga.


11
A prima vista sembra più pulito, ma presenta lo svantaggio di richiedere al manutentore di mantenere sincronizzato l'uno o l'altro di entrambi gli elenchi. Nell'esempio dei giorni della settimana, non sembra probabile. Ma in generale, è possibile inserire un nuovo valore o eliminarlo e le due liste potrebbero non essere sincronizzate, nel qual caso potrebbero essere introdotti bug sottili.
Brent Faust,

1
Secondo il commento precedente, il rischio è che le due diverse liste possano silenziosamente andare fuori sincrono. Sebbene non sia un problema per il tuo piccolo esempio attuale, se ci sono molti più membri (come nelle dozzine a centinaia), le probabilità che le due liste silenziosamente non siano più sincronizzate sono sostanzialmente più alte. Inoltre scala.Enumeration non può beneficiare del tempo esaustivo di compilazione di Scala per la corrispondenza di avvertenze / errori. Ho creato una risposta StackOverflow, che contiene una soluzione di eseguire un controllo di runtime per garantire le due liste rimangono in sincronia: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

17

È possibile utilizzare una classe astratta sigillata anziché l'enumerazione, ad esempio:

sealed abstract class Constraint(val name: String, val verifier: Int => Boolean)

case object NotTooBig extends Constraint("NotTooBig", (_ < 1000))
case object NonZero extends Constraint("NonZero", (_ != 0))
case class NotEquals(x: Int) extends Constraint("NotEquals " + x, (_ != x))

object Main {

  def eval(ctrs: Seq[Constraint])(x: Int): Boolean =
    (true /: ctrs){ case (accum, ctr) => accum && ctr.verifier(x) }

  def main(args: Array[String]) {
    val ctrs = NotTooBig :: NotEquals(5) :: Nil
    val evaluate = eval(ctrs) _

    println(evaluate(3000))
    println(evaluate(3))
    println(evaluate(5))
  }

}

È anche possibile un tratto sigillato con oggetti caso.
Ashalynd,

2
Il modello "tratto sigillato + oggetti maiuscole" presenta problemi dettagliati in una risposta StackOverflow. Tuttavia, ho fatto capire come risolvere tutti i problemi relativi a questo modello che è anche coperto nel thread: stackoverflow.com/a/25923651/501113
chaotic3quilibrium


2

Dopo aver fatto ricerche approfondite su tutte le opzioni relative alle "enumerazioni" in Scala, ho pubblicato una panoramica molto più completa di questo dominio su un altro thread StackOverflow . Include una soluzione al modello "trait + seal case case" in cui ho risolto il problema di ordinazione di inizializzazione di oggetti / classe JVM.



1

In Scala è molto comodo con https://github.com/lloydmeta/enumeratum

Il progetto è davvero buono con esempi e documentazione

Solo questo esempio dai loro documenti dovrebbe farti interessare

import enumeratum._

sealed trait Greeting extends EnumEntry

object Greeting extends Enum[Greeting] {

  /*
   `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`

   You use it to implement the `val values` member
  */
  val values = findValues

  case object Hello   extends Greeting
  case object GoodBye extends Greeting
  case object Hi      extends Greeting
  case object Bye     extends Greeting

}

// Object Greeting has a `withName(name: String)` method
Greeting.withName("Hello")
// => res0: Greeting = Hello

Greeting.withName("Haro")
// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)

// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]
Greeting.withNameOption("Hello")
// => res1: Option[Greeting] = Some(Hello)

Greeting.withNameOption("Haro")
// => res2: Option[Greeting] = None

// It is also possible to use strings case insensitively
Greeting.withNameInsensitive("HeLLo")
// => res3: Greeting = Hello

Greeting.withNameInsensitiveOption("HeLLo")
// => res4: Option[Greeting] = Some(Hello)

// Uppercase-only strings may also be used
Greeting.withNameUppercaseOnly("HELLO")
// => res5: Greeting = Hello

Greeting.withNameUppercaseOnlyOption("HeLLo")
// => res6: Option[Greeting] = None

// Similarly, lowercase-only strings may also be used
Greeting.withNameLowercaseOnly("hello")
// => res7: Greeting = Hello

Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)
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.