Comment modéliser des types d'énumération sûrs?

311

Scala n'a pas de type sûr enumcomme Java. Étant donné un ensemble de constantes liées, quelle serait la meilleure façon dans Scala de représenter ces constantes?

Jesper
la source
2
Pourquoi ne pas simplement utiliser Java Enum? C'est l'une des rares choses que je préfère encore utiliser Java simple.
Max
1
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

Réponses:

187

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

Exemple d'utilisation

  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
  }
skaffman
la source
2
Sérieusement, l'application ne doit pas être utilisée. Ce n'était PAS corrigé; une nouvelle classe, App, a été introduite, qui n'a pas les problèmes mentionnés par Schildmeijer. Il en est de même de "l'objet foo étend l'application {...}" et vous avez un accès immédiat aux arguments de ligne de commande via la variable args.
AmigoNico
scala.Enumeration (qui est ce que vous utilisez dans votre exemple de code "object WeekDay" ci-dessus) n'offre pas de correspondance de modèle exhaustive. J'ai recherché tous les différents modèles d'énumération actuellement utilisés dans Scala et j'en donne et un aperçu dans cette réponse StackOverflow (y compris un nouveau modèle qui offre le meilleur des deux scala.Enumeration et le modèle "scellé trait + objet de cas": stackoverflow. com / a / 25923651/501113
chaotic3quilibrium
377

Je dois dire que l'exemple copié de la documentation Scala par skaffman ci-dessus est d'une utilité limitée dans la pratique (vous pourriez aussi bien utiliser case objects).

Afin d'obtenir quelque chose qui ressemble le plus à un Java Enum(c'est-à-dire avec des méthodes sensées toStringet valueOf- peut-être que vous persistez les valeurs d'énumération dans une base de données), vous devez le modifier un peu. Si vous aviez utilisé le code de skaffman :

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

Attendu qu'en utilisant la déclaration suivante:

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

Vous obtenez des résultats plus sensibles:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
la source
7
Btw. La méthode valueOf est maintenant morte :-(
greenoldman
36
valueOfLe remplacement de @macias est withName, qui ne retourne pas d'option, et lance un NSE s'il n'y a pas de correspondance. Qu'est-ce que!
Bluu
6
@Bluu Vous pouvez ajouter valueOf yourself: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) pour avoir une option
centr
@centr Lorsque j'essaye de créer un Map[Weekday.Weekday, Long]et d'ajouter une valeur, Monle compilateur génère une erreur de type non valide. Jour de semaine prévu. Pourquoi cela arrive-t-il?
Sohaib
@Sohaib Ce devrait être Map [Weekday.Value, Long].
CENTR
99

Il y a plusieurs façons de faire.

1) Utilisez des symboles. Cependant, cela ne vous donnera aucune sécurité de type, à part qu'il n'accepte pas les non-symboles où un symbole est attendu. Je ne le mentionne ici que pour être complet. Voici un exemple d'utilisation:

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) Utilisation de la classe Enumeration:

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

ou, si vous devez le sérialiser ou l'afficher:

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

Cela peut être utilisé comme ceci:

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 /

Malheureusement, cela ne garantit pas que tous les matchs sont pris en compte. Si j'avais oublié de mettre Row ou Column dans le match, le compilateur Scala ne m'aurait pas prévenu. Cela me donne donc une certaine sécurité de type, mais pas autant que ce qui peut être gagné.

3) Objets de cas:

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

Maintenant, si je laisse un cas sur a match, le compilateur m'avertira:

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

    what match {
    ^
one warning found

Il est utilisé à peu près de la même manière et n'a même pas besoin d'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 /

Vous vous demandez peut-être alors pourquoi utiliser une énumération plutôt que des objets de cas. En fait, les objets de cas présentent de nombreux avantages, comme ici. La classe Enumeration, cependant, possède de nombreuses méthodes Collection, telles que des éléments (itérateur sur Scala 2.8), qui renvoie un Iterator, une carte, un flatMap, un filtre, etc.

Cette réponse est essentiellement une partie sélectionnée de cet article dans mon blog.

Daniel C. Sobral
la source
"... n'acceptant pas les non-symboles où un symbole est attendu"> Je suppose que vous voulez dire que les Symbolinstances ne peuvent pas avoir d'espaces ou de caractères spéciaux. La plupart des gens lors de leur première rencontre avec la Symbolclasse le pensent probablement, mais c'est en fait incorrect. Symbol("foo !% bar -* baz")compile et fonctionne parfaitement bien. En d'autres termes, vous pouvez parfaitement créer des Symbolinstances enveloppant n'importe quelle chaîne (vous ne pouvez tout simplement pas le faire avec le sucre syntaxique "coma unique"). La seule chose qui Symbolgarantit est l'unicité d'un symbole donné, ce qui le rend légèrement plus rapide à comparer et à faire correspondre.
Régis Jean-Gilles
@ RégisJean-Gilles Non, je veux dire que vous ne pouvez pas passer un String, par exemple, comme argument à un Symbolparamètre.
Daniel C.Sobral
Oui, j'ai compris cette partie, mais c'est un point discutable si vous remplacez Stringpar une autre classe qui est essentiellement un wrapper autour d'une chaîne et peut être librement convertie dans les deux sens (comme c'est le cas pour Symbol). Je suppose que c'est ce que vous vouliez dire en disant "Cela ne vous donnera aucune sécurité de type", ce n'était tout simplement pas très clair étant donné que OP a explicitement demandé des solutions sûres de type. Je ne savais pas si au moment de la rédaction de cet article, vous saviez que non seulement ce type n'est pas sûr car ce ne sont pas du tout des énumérations, mais aussi Symbol qu'il ne garantit même pas que l'argument passé n'aura pas de caractères spéciaux.
Régis Jean-Gilles
1
Pour élaborer, lorsque vous dites «n'accepter pas de non-symboles où un symbole est attendu», cela peut être lu comme «n'acceptant pas de valeurs qui ne sont pas des instances de Symbol» (ce qui est évidemment vrai) ou «n'acceptant pas de valeurs qui ne sont pas identifiant comme simple des chaînes, alias « symboles » »( ce qui est faux, et est une idée fausse que à peu près tout le monde a la première fois que nous rencontrons des symboles scala, en raison du fait que la première rencontre est bien la spéciale 'foonotation qui ne se opposent chaînes non identifiantes). C'est cette idée fausse que je voulais dissiper pour tout futur lecteur.
Régis Jean-Gilles
@ RégisJean-Gilles Je voulais dire le premier, celui qui est évidemment vrai. Je veux dire, c'est évidemment vrai pour toute personne habituée à la frappe statique. À l'époque, il y avait beaucoup de discussions sur les mérites relatifs du typage statique et "dynamique", et beaucoup de gens intéressés par Scala venaient d'un milieu de typage dynamique, donc j'ai pensé que cela n'allait pas de soi. Je ne penserais même pas à faire cette remarque de nos jours. Personnellement, je pense que le symbole de Scala est moche et redondant, et ne l'utilise jamais. Je vote pour votre dernier commentaire, car c'est un bon point.
Daniel C.Sobral
52

Une façon un peu moins verbeuse de déclarer les énumérations nommées:

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

Bien sûr, le problème ici est que vous devrez synchroniser l'ordre des noms et des valeurs, ce qui est plus facile à faire si le nom et la valeur sont déclarés sur la même ligne.

Walter Chang
la source
11
Cela semble plus propre à première vue, mais présente l'inconvénient d'exiger du responsable qu'il synchronise l'odeur des deux listes. Pour l'exemple des jours de la semaine, cela ne semble pas probable. Mais en général, la nouvelle valeur pourrait être insérée, ou supprimée et les deux listes pourraient être désynchronisées, auquel cas, des bogues subtils pourraient être introduits.
Brent Faust
1
Selon le commentaire précédent, le risque est que les deux listes différentes puissent se désynchroniser en silence. Bien que ce ne soit pas un problème pour votre petit exemple actuel, s'il y a beaucoup plus de membres (comme des dizaines à des centaines), les chances que les deux listes se désynchronisent silencieusement sont considérablement plus élevées. De plus, scala.Enumeration ne peut pas bénéficier des avertissements / erreurs de correspondance de motifs exhaustifs de Scala. J'ai créé une réponse StackOverflow qui contient une solution effectuant une vérification de l'exécution pour s'assurer que les deux listes restent synchronisées: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Vous pouvez utiliser une classe abstraite scellée au lieu de l'énumération, par exemple:

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

}
Ron
la source
Un trait scellé avec des objets de cas est également une possibilité.
Ashalynd
2
Le modèle "scellé trait + objets de cas" a des problèmes que je détaille dans une réponse StackOverflow. Cependant, j'ai
trouvé
7

vient de découvrir enumeratum . c'est assez étonnant et tout aussi étonnant ce n'est pas plus connu!

praticien
la source
2

Après avoir fait des recherches approfondies sur toutes les options autour des "énumérations" dans Scala, j'ai posté un aperçu beaucoup plus complet de ce domaine sur un autre thread StackOverflow . Il comprend une solution au modèle "trait scellé + objet cas" où j'ai résolu le problème de commande d'initialisation de classe / objet JVM.

chaotic3quilibrium
la source
1

Dotty (Scala 3) aura des énumérations natives prises en charge. Vérifiez ici et ici .

zéronone
la source
1

À Scala, il est très à l'aise avec https://github.com/lloydmeta/enumeratum

Le projet est vraiment bon avec des exemples et de la documentation

Cet exemple de leur documentation devrait vous intéresser

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)
Dmitriy Kuzkin
la source