¿Cómo modelar tipos de enumeración de tipo seguro?

311

Scala no tiene s de tipo seguro enumcomo Java. Dado un conjunto de constantes relacionadas, ¿cuál sería la mejor manera en Scala para representar esas constantes?

Jesper
fuente
2
¿Por qué no solo usar java enum? Esta es una de las pocas cosas que todavía prefiero usar Java simple.
Max
1
He escrito una pequeña descripción general sobre Scala Enumeración y alternativas, puede que le resulte útil: pedrorijo.com/blog/scala-enums/
pedrorijo91

Respuestas:

187

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

Ejemplo de uso

  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
fuente
2
En serio, la aplicación no debe ser utilizada. NO fue arreglado; Se introdujo una nueva clase, App, que no tiene los problemas mencionados por Schildmeijer. Así que "object foo extiende la aplicación {...}" Y tiene acceso inmediato a los argumentos de la línea de comandos a través de la variable args.
AmigoNico
scala.Enumeration (que es lo que está utilizando en su ejemplo de código "object WeekDay" anterior) no ofrece una concordancia exhaustiva de patrones. He investigado todos los diferentes patrones de enumeración que se usan actualmente en Scala y les doy una descripción general de ellos en esta respuesta de StackOverflow (incluido un nuevo patrón que ofrece lo mejor de ambos scala.Enumeration y el patrón "rasgo sellado + objeto de caso": stackoverflow. com / a / 25923651/501113
chaotic3quilibrium
377

Debo decir que el ejemplo copiado de la documentación de Scala por skaffman anterior es de utilidad limitada en la práctica (también podría usar case objects).

Para obtener algo que se parezca más a un Java Enum(es decir, con métodos toStringy valueOfmétodos razonables , quizás persista los valores de enumeración en una base de datos), debe modificarlo un poco. Si hubiera usado el código de skaffman :

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

Mientras que utilizando la siguiente declaración:

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

Obtienes resultados más sensibles:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
fuente
77
Por cierto. El método valueOf ahora está muerto :-(
greenoldman
36
valueOfEl reemplazo de @macias es withName, que no devuelve una Opción, y arroja un NSE si no hay coincidencia. ¡Que!
Bluu
66
@Bluu Puede agregar valorDe usted mismo: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) para tener una Opción
centr
@centr Cuando intento crear Map[Weekday.Weekday, Long]y agregar un valor, le digo Monque el compilador arroja un error de tipo no válido. Día de la semana esperado. ¿Valor encontrado el día de la semana? ¿Por qué pasó esto?
Sohaib
@Sohaib Debería ser Mapa [Weekday.Value, Long].
centr
99

Hay muchas formas de hacerlo.

1) Usa símbolos. Sin embargo, no le dará ningún tipo de seguridad, aparte de no aceptar no símbolos donde se espera un símbolo. Solo lo estoy mencionando aquí para completar. Aquí hay un ejemplo de uso:

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) Usando la clase Enumeration:

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

o, si necesita serializarlo o mostrarlo:

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

Esto se puede usar así:

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 /

Lamentablemente, no garantiza que se tengan en cuenta todas las coincidencias. Si olvidé poner Row o Column en el partido, el compilador de Scala no me lo habría advertido. Por lo tanto, me da cierto tipo de seguridad, pero no tanto como se puede obtener.

3) Objetos de caso:

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

Ahora, si dejo un caso en un match, el compilador me avisará:

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

    what match {
    ^
one warning found

Se usa casi de la misma manera, y ni siquiera necesita 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 /

Podría preguntarse, entonces, ¿por qué usar una Enumeración en lugar de objetos de caso? De hecho, los objetos de caso tienen ventajas muchas veces, como aquí. Sin embargo, la clase Enumeration tiene muchos métodos de colección, como elementos (iterador en Scala 2.8), que devuelve un iterador, mapa, flatMap, filtro, etc.

Esta respuesta es esencialmente una parte seleccionada de este artículo en mi blog.

Daniel C. Sobral
fuente
"... no se aceptan no símbolos donde se espera un símbolo"> Supongo que quiere decir que las Symbolinstancias no pueden tener espacios o caracteres especiales. La mayoría de las personas cuando se encuentran por primera vez con la Symbolclase probablemente piensan eso, pero en realidad es incorrecto. Symbol("foo !% bar -* baz")compila y funciona perfectamente bien. En otras palabras, puede crear perfectamente Symbolinstancias que envuelvan cualquier cadena (simplemente no puede hacerlo con el azúcar sintáctico de "coma único"). Lo único que Symbolgarantiza es la singularidad de cualquier símbolo dado, lo que lo hace marginalmente más rápido para comparar y combinar.
Régis Jean-Gilles
@ RégisJean-Gilles No, quiero decir que no puede pasar un String, por ejemplo, como argumento a un Symbolparámetro.
Daniel C. Sobral
Sí, entendí esa parte, pero es un punto discutible si lo reemplazas Stringcon otra clase que básicamente es un envoltorio alrededor de una cadena y se puede convertir libremente en ambas direcciones (como es el caso Symbol). Supongo que a eso se refería cuando decía "No le dará ningún tipo de seguridad", simplemente no estaba muy claro dado que OP solicitó explícitamente soluciones de tipo seguro. No estaba segura de si en el momento de escribir que sabía que no sólo no es un tipo seguro, porque esos no son enumeraciones en absoluto, sino también Symbol s ni siquiera garantía de que el argumento pasado no tendrá caracteres especiales.
Régis Jean-Gilles
1
Para elaborar, cuando dices "no aceptar no símbolos donde se espera un símbolo", puede leerse como "no aceptar valores que no son instancias de Symbol" (lo cual es obviamente cierto) o "no aceptar valores que no son identificador-como llanura cuerdas, también conocido como 'símbolos'"(que no es cierto, y es una idea errónea de que casi nadie tiene la primera vez que nos encontramos con símbolos Scala, debido al hecho de que el primer encuentro es a pesar de la especial 'foode notación, que hace oponen cadenas sin identificador). Este es un concepto erróneo que quería disipar para cualquier futuro lector.
Régis Jean-Gilles
@ RégisJean-Gilles Me refería al primero, el que obviamente es cierto. Quiero decir, obviamente es cierto para cualquiera que esté acostumbrado a la escritura estática. En aquel entonces hubo mucha discusión sobre los méritos relativos de la escritura estática y "dinámica", y mucha gente interesada en Scala provenía de un entorno de escritura dinámica, así que pensé que no pasó sin decirlo. Ni siquiera pensaría en hacer ese comentario hoy en día. Personalmente, creo que el Símbolo de Scala es feo y redundante, y nunca lo use. Estoy votando tu último comentario, ya que es un buen punto.
Daniel C. Sobral
52

Una forma un poco menos detallada de declarar enumeraciones nombradas:

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

Por supuesto, el problema aquí es que necesitará mantener el orden de los nombres y valores sincronizados, lo que es más fácil de hacer si se declaran nombre y valor en la misma línea.

Walter Chang
fuente
11
Esto parece más limpio a primera vista, pero tiene la desventaja de requerir que el responsable mantenga sincronizado el orden de ambas listas. Para el ejemplo de los días de la semana, no parece probable. Pero, en general, se puede insertar un nuevo valor, o se puede eliminar uno y las dos listas pueden estar fuera de sincronización, en cuyo caso, se pueden introducir errores sutiles.
Brent Faust
1
Según el comentario anterior, el riesgo es que las dos listas diferentes pueden sincronizarse silenciosamente. Si bien no es un problema para su pequeño ejemplo actual, si hay muchos más miembros (como decenas a cientos), las probabilidades de que las dos listas se desincronicen en silencio es sustancialmente mayor. También scala. La enumeración no puede beneficiarse de las advertencias / errores exhaustivos de coincidencia de patrones de tiempo de compilación de Scala. He creado una respuesta StackOverflow que contiene una solución que realiza una comprobación de tiempo de ejecución para garantizar que las dos listas permanezcan sincronizadas: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Puede usar una clase abstracta sellada en lugar de la enumeración, por ejemplo:

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
fuente
El rasgo sellado con objetos de caja también es una posibilidad.
Ashalynd
2
El patrón "rasgo cerrado + objetos de caso" tiene problemas que detallo en una respuesta de StackOverflow. Sin embargo, descubrí cómo resolver todos los problemas relacionados con este patrón que también está cubierto en el hilo: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
7

acabo de descubrir enumeratum . ¡Es bastante sorprendente e igualmente sorprendente, no es más conocido!

practechal
fuente
2

Después de hacer una extensa investigación sobre todas las opciones en torno a las "enumeraciones" en Scala, publiqué una descripción mucho más completa de este dominio en otro hilo de StackOverflow . Incluye una solución al patrón "rasgo sellado + objeto de caso" en el que he resuelto el problema de ordenación de inicialización de clase / objeto JVM.

equilibrio caótico
fuente
1

Dotty (Scala 3) tendrá enums nativos compatibles. Mira aquí y aquí .

zeronone
fuente
1

En Scala es muy cómodo con https://github.com/lloydmeta/enumeratum

El proyecto es realmente bueno con ejemplos y documentación.

Solo este ejemplo de sus documentos debería hacerte interesado en

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
fuente