Objetos de caso vs Enumeraciones en Scala

231

¿Hay alguna guía de mejores prácticas sobre cuándo usar las clases de casos? (u objetos de casos) en lugar de extender la enumeración en Scala?

Parecen ofrecer algunos de los mismos beneficios.

Alex Miller
fuente
2
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
1
Ver también el Scala 3 basado en Dottyenum (para mediados de 2020).
VonC

Respuestas:

223

Una gran diferencia es que Enumerationviene con soporte para crear instancias de algunos nameString. Por ejemplo:

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

Entonces puedes hacer:

val ccy = Currency.withName("EUR")

Esto es útil cuando se desea conservar las enumeraciones (por ejemplo, en una base de datos) o crearlas a partir de datos que residen en archivos. Sin embargo, encuentro en general que las enumeraciones son un poco torpes en Scala y tienen la sensación de un complemento incómodo, por lo que ahora tiendo a usar case objects. A case objectes más flexible que una enumeración:

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

case class UnknownCurrency(name: String) extends Currency

Entonces ahora tengo la ventaja de ...

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

Como señaló @ chaotic3quilibrium (con algunas correcciones para facilitar la lectura):

Con respecto al patrón "UnknownCurrency (code)", hay otras formas de manejar el hecho de no encontrar una cadena de código de moneda que "romper" la naturaleza del conjunto cerrado del Currencytipo. UnknownCurrencyser de tipo Currencyahora puede colarse en otras partes de una API.

Es aconsejable sacar ese caso al exterior Enumerationy hacer que el cliente trate con un Option[Currency]tipo que indique claramente que realmente hay un problema de coincidencia y "aliente" al usuario de la API a que lo resuelva por sí mismo.

Para seguir las otras respuestas aquí, los principales inconvenientes de case objects sobre Enumerations son:

  1. No se puede iterar sobre todas las instancias de la "enumeración" . Este es ciertamente el caso, pero me ha resultado extremadamente raro en la práctica que sea necesario.

  2. No se puede crear una instancia fácilmente del valor persistente . Esto también es cierto, pero, excepto en el caso de grandes enumeraciones (por ejemplo, todas las monedas), esto no presenta una gran sobrecarga.

oxbow_lakes
fuente
10
La otra diferencia es que la enumeración enumeración se ordena fuera de la caja, mientras que la enumeración basada en objetos de caso obviamente no
om-nom-nom
1
Otro punto para los objetos de caso es si le importa la interoperabilidad de Java. La Enumeración devolvería los valores como Enumeration.Value, por lo tanto 1) requiere scala-library, 2) pierde la información de tipo real.
juanmirocks
77
@oxbow_lakes Con respecto al punto 1, específicamente esta parte "... En la práctica, me pareció extremadamente raro que esto sea necesario": Aparentemente, rara vez haces mucho trabajo de interfaz de usuario. Este es un caso de uso extremadamente común; mostrando una lista (desplegable) de miembros de enumeración válidos para elegir.
chaotic3quilibrium
No entiendo el tipo de elemento que coincide trade.ccycon el ejemplo de rasgo sellado.
rloth
y no case objectgenera una huella de código mayor (~ 4x) que Enumeration? Distinción útil especialmente para scala.jsproyectos que necesitan una pequeña huella.
ecoe
69

ACTUALIZACIÓN: Se ha creado una nueva solución basada en macro que es muy superior a la solución que describo a continuación. Recomiendo usar esta nueva solución basada en macro . Y parece que los planes para Dotty harán que este estilo de solución de enumeración sea parte del lenguaje. Whoohoo!

Resumen:
Existen tres patrones básicos para intentar reproducir Java Enumdentro de un proyecto Scala. Dos de los tres patrones; directamente utilizando Java Enumy scala.Enumeration, no son capaces de habilitar la exhaustiva coincidencia de patrones de Scala. Y el tercero; "rasgo sellado + objeto de caso", sí ... pero tiene complicaciones de inicialización de clase / objeto JVM que resultan en una generación de índice ordinal inconsistente.

He creado una solución con dos clases; Enumeración y Enumeración Decorado , ubicado en este Gist . No publiqué el código en este hilo ya que el archivo para Enumeration era bastante grande (+400 líneas - contiene muchos comentarios que explican el contexto de implementación).

Detalles:
la pregunta que hace es bastante general; "... cuándo usar caseclasesobjects vs extender [scala.]Enumeration". Y resulta que hay MUCHAS respuestas posibles, cada respuesta depende de las sutilezas de los requisitos específicos del proyecto que tenga. La respuesta se puede reducir a tres patrones básicos.

Para comenzar, asegurémonos de que trabajamos desde la misma idea básica de qué es una enumeración. Definamos una enumeración principalmente en términos de lo Enumproporcionado a partir de Java 5 (1.5) :

  1. Contiene un conjunto cerrado de miembros nombrados ordenados naturalmente
    1. Hay un número fijo de miembros.
    2. Los miembros se ordenan naturalmente y se indexan explícitamente
      • En lugar de ser ordenado en base a algunos criterios inaceptables para miembros
    3. Cada miembro tiene un nombre único dentro del conjunto total de todos los miembros
  2. Todos los miembros pueden iterarse fácilmente en función de sus índices
  3. Un miembro puede recuperarse con su nombre (distingue entre mayúsculas y minúsculas)
    1. Sería bastante bueno si un miembro también pudiera ser recuperado con su nombre insensible a mayúsculas y minúsculas
  4. Un miembro puede ser recuperado con su índice
  5. Los miembros pueden usar la serialización de manera fácil, transparente y eficiente
  6. Los miembros pueden ampliarse fácilmente para contener datos adicionales asociados de soltería
  7. Pensando más allá de Java Enum, sería bueno poder explícitamente aprovechar la comprobación de exhaustividad de coincidencia de patrones de Scala para una enumeración

A continuación, echemos un vistazo a las versiones resumidas de los tres patrones de solución más comunes publicados:

A) Realmente usando directamente el patrón JavaEnum (en un proyecto mixto 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;
  }
}

Los siguientes elementos de la definición de enumeración no están disponibles:

  1. 3.1 - Sería bastante bueno si un miembro también pudiera ser recuperado con su nombre que no distingue entre mayúsculas y minúsculas
  2. 7 - Pensando más allá de la enumeración de Java, sería bueno poder explícitamente aprovechar la comprobación de exhaustividad de coincidencia de patrones de Scala para una enumeración

Para mis proyectos actuales, no tengo el beneficio de correr riesgos en torno a la ruta del proyecto mixto Scala / Java. E incluso si pudiera elegir hacer un proyecto mixto, el elemento 7 es crítico para permitirme detectar problemas de tiempo de compilación si / cuando agrego / elimino miembros de enumeración, o estoy escribiendo algún código nuevo para tratar con los miembros de enumeración existentes.


B) Usando el patrón " 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}
}

Los siguientes elementos de la definición de enumeración no están disponibles:

  1. 1.2 - Los miembros se ordenan naturalmente y se indexan explícitamente
  2. 2 - Todos los miembros pueden iterarse fácilmente en función de sus índices
  3. 3 - Se puede recuperar un miembro con su nombre (distingue entre mayúsculas y minúsculas)
  4. 3.1 - Sería bastante bueno si un miembro también pudiera ser recuperado con su nombre que no distingue entre mayúsculas y minúsculas
  5. 4 - Se puede recuperar un miembro con su índice

Es discutible que realmente cumpla con los elementos de definición de enumeración 5 y 6. Para 5, es difícil afirmar que es eficiente. Para 6, no es realmente fácil extenderlo para contener datos adicionales asociados de soltería.


C) Usando el scala.Enumerationpatrón (inspirado en esta respuesta de 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]
}

Los siguientes elementos de la definición de enumeración no están disponibles (resulta ser idéntico a la lista para usar directamente la enumeración de Java):

  1. 3.1 - Sería bastante bueno si un miembro también pudiera ser recuperado con su nombre que no distingue entre mayúsculas y minúsculas
  2. 7 - Pensando más allá de la enumeración de Java, sería bueno poder explícitamente aprovechar la comprobación de exhaustividad de coincidencia de patrones de Scala para una enumeración

Nuevamente para mis proyectos actuales, el ítem 7 es crítico para permitirme detectar problemas de tiempo de compilación si / cuando agrego / elimino miembros de enumeración, o estoy escribiendo algún código nuevo para tratar con los miembros de enumeración existentes.


Entonces, dada la definición anterior de una enumeración, ninguna de las tres soluciones anteriores funciona ya que no proporcionan todo lo que se describe en la definición de enumeración anterior:

  1. Java Enum directamente en un proyecto mixto Scala / Java
  2. "rasgo sellado + objetos de caja"
  3. scala.Enumeración

Cada una de estas soluciones puede eventualmente ser modificada / ampliada / refactorizada para intentar cubrir algunos de los requisitos faltantes de cada uno. Sin embargo, ni Java Enumni las scala.Enumerationsoluciones pueden expandirse lo suficiente como para proporcionar el ítem 7. Y para mis propios proyectos, este es uno de los valores más convincentes de usar un tipo cerrado dentro de Scala. Prefiero encarecidamente las advertencias / errores de tiempo de compilación para indicar que tengo un espacio / problema en mi código en lugar de tener que recogerlo de una excepción / falla de tiempo de ejecución de producción.


En ese sentido, me puse a trabajar con la case objectvía para ver si podía producir una solución que cubriera toda la definición de enumeración anterior. El primer desafío fue superar el núcleo del problema de inicialización de clase / objeto JVM (cubierto en detalle en esta publicación de StackOverflow ). Y finalmente pude encontrar una solución.

Como mi solución son dos rasgos; Enumeration y EnumerationDecorated , y dado que el Enumerationrasgo tiene más de +400 líneas de largo (muchos comentarios explican el contexto), renuncio a pegarlo en este hilo (lo que haría que se extienda considerablemente por la página). Para más detalles, vaya directamente a Gist .

Así es como se ve la solución usando la misma idea de datos que la anterior (versión totalmente comentada disponible aquí ) e implementada en 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
}

Este es un ejemplo de uso de un nuevo par de rasgos de enumeración que creé (ubicado en este Gist ) para implementar todas las capacidades deseadas y descritas en la definición de enumeración.

Una preocupación expresada es que los nombres de los miembros de la enumeración deben repetirse ( decorationOrderedSeten el ejemplo anterior). Si bien lo minimicé a una sola repetición, no pude ver cómo hacerlo aún menos debido a dos problemas:

  1. La inicialización de objeto / clase JVM para este objeto particular / modelo de objeto de caso no está definida (consulte este hilo de Stackoverflow )
  2. El contenido devuelto por el método getClass.getDeclaredClassestiene un orden indefinido (y es poco probable que esté en el mismo orden que las case objectdeclaraciones en el código fuente)

Dados estos dos problemas, tuve que renunciar a intentar generar un pedido implícito y tuve que exigir explícitamente que el cliente lo defina y lo declare con algún tipo de noción de conjunto ordenado. Como las colecciones de Scala no tienen una implementación de conjunto ordenado por inserción, lo mejor que pude hacer fue usar un Listy luego verificar que realmente era un conjunto. No es como hubiera preferido haber logrado esto.

Y teniendo en cuenta el diseño requiere esta segunda lista / pedido conjunto val, dado el ChessPiecesEnhancedDecoratedejemplo anterior, era posible añadir case object PAWN2 extends Membery luego olvidarse de añadir Decoration(PAWN2,'P2', 2)a decorationOrderedSet. Por lo tanto, hay una comprobación de tiempo de ejecución para verificar que la lista no es solo un conjunto, sino que contiene TODOS los objetos de caso que extienden el sealed trait Member. Esa fue una forma especial de reflexión / macro infierno para trabajar.


Por favor, deje comentarios y / o comentarios sobre el Gist .

equilibrio caótico
fuente
Ahora he lanzado la primera versión de la biblioteca ScalaOlio (GPLv3) que contiene versiones más actualizadas de ambos org.scalaolio.util.Enumerationy org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium
Y para ir directamente al repositorio ScalaOlio en Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium el
55
Esta es una respuesta de calidad y mucho que sacar de ella. Gracias
angabriel
1
Parece que Odersky quiere actualizar Dotty (futuro Scala 3.0) con una enumeración nativa. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium
62

Los objetos de caso ya devuelven su nombre para sus métodos toString, por lo que no es necesario pasarlo por separado. Aquí hay una versión similar a la de jho (métodos de conveniencia omitidos por brevedad):

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

Los objetos son vagos; al usar vals en su lugar, podemos soltar la lista pero tenemos que repetir el nombre:

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 no le importa hacer trampa, puede precargar sus valores de enumeración usando la API de reflexión o algo así como Google Reflections. Los objetos de caso no perezosos le dan la sintaxis más limpia:

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
}

Agradable y limpio, con todas las ventajas de las clases de casos y las enumeraciones de Java. Personalmente, defino los valores de enumeración fuera del objeto para que coincidan mejor con el código idiomático de Scala:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
GatesDA
fuente
3
una pregunta: la última solución se llama "objetos de caso no perezosos" pero en este caso los objetos no se cargan hasta que los usamos: ¿por qué llama a esta solución no perezoso?
Seb Cesbron
2
@Noel, debe usar: pegar para pegar toda la jerarquía sellada en el REPL. Si no lo hace, la línea única con la clase / rasgo base sellada cuenta como un solo archivo, se sella inmediatamente y no se puede extender en la siguiente línea.
Jürgen Strobel
2
@GatesDA Solo su primer fragmento de código no tiene un error (ya que explícitamente requiere que el cliente declare y defina valores. Tanto su segunda como la tercera solución tienen el error sutil que describí en mi último comentario (si el cliente accede a Moneda) .GBP directamente y primero, la Lista de valores "estará fuera de servicio"). He explorado ampliamente el dominio de enumeración de Scala y lo he cubierto en detalle en mi respuesta a este mismo hilo: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
1
Quizás uno de los inconvenientes de este enfoque (en comparación con Java Enums de todos modos) es que cuando escribe Moneda <dot> en IDE, no muestra las opciones disponibles.
Ivan Balashov
1
Como @SebCesbron mencionó, los objetos del caso son vagos aquí. Entonces, si llamo Currency.values, solo obtengo valores a los que he accedido anteriormente. ¿Hay alguna forma de evitar eso?
Sasgorilla
27

Las ventajas de usar clases de caso sobre Enumeraciones son:

  • Cuando se utilizan clases de mayúsculas y minúsculas selladas, el compilador de Scala puede determinar si la coincidencia está completamente especificada, por ejemplo, cuándo se adoptan todas las coincidencias posibles en la declaración de coincidencia. Con las enumeraciones, el compilador Scala no puede decir.
  • Las clases de casos admiten naturalmente más campos que una enumeración basada en valores que admite un nombre y una ID.

Las ventajas de usar Enumeraciones en lugar de clases de casos son:

  • Las enumeraciones generalmente serán un poco menos de código para escribir.
  • Las enumeraciones son un poco más fáciles de entender para alguien nuevo en Scala, ya que son frecuentes en otros idiomas

Entonces, en general, si solo necesita una lista de constantes simples por nombre, use enumeraciones. De lo contrario, si necesita algo un poco más complejo o desea que la seguridad adicional del compilador le diga si tiene todas las coincidencias especificadas, use las clases de casos.

Aaron
fuente
15

ACTUALIZACIÓN: El siguiente código tiene un error, que se describe aquí . El siguiente programa de prueba funciona, pero si fuera a usar DayOfWeek.Mon (por ejemplo) antes de DayOfWeek, fallaría porque DayOfWeek no se ha inicializado (el uso de un objeto interno no hace que se inicialice un objeto externo). Todavía puede usar este código si hace algo como val enums = Seq( DayOfWeek )en su clase principal, forzando la inicialización de sus enumeraciones, o puede usar las modificaciones de chaotic3quilibrium. ¡Esperamos una enumeración basada en macros!


Si tu quieres

  • advertencias sobre coincidencias de patrones no exhaustivos
  • una ID Int asignada a cada valor de enumeración, que puede controlar opcionalmente
  • Una lista inmutable de los valores de enumeración, en el orden en que se definieron
  • Un mapa inmutable de nombre a valor de enumeración
  • Un mapa inmutable de id a enum value
  • lugares para pegar métodos / datos para todos o valores de enumeración particulares, o para la enumeración como un todo
  • valores de enumeración ordenados (para que pueda probar, por ejemplo, si día <miércoles)
  • la capacidad de extender una enumeración para crear otras

entonces lo siguiente puede ser de interés. Comentarios bienvenidos.

En esta implementación hay clases base abstractas Enum y EnumVal, que usted extiende. Veremos esas clases en un minuto, pero primero, así es como definiría una enumeración:

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

Tenga en cuenta que debe usar cada valor de enumeración (llame a su método de aplicación) para darle vida. [Desearía que los objetos internos no fueran perezosos a menos que pida específicamente que lo sean. Yo creo que.]

Por supuesto, podríamos agregar métodos / datos a DayOfWeek, Val o los objetos de caso individuales si así lo deseáramos.

Y así es como usarías tal enumeración:

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

}

Esto es lo que obtienes cuando lo compilas:

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

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

Puede reemplazar "coincidencia de día" con "coincidencia (día: @ no comprobado)" donde no desea tales advertencias, o simplemente incluir un caso general al final.

Cuando ejecuta el programa anterior, obtiene esta salida:

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

Tenga en cuenta que, dado que la Lista y los Mapas son inmutables, puede eliminar fácilmente elementos para crear subconjuntos, sin romper la enumeración.

Aquí está la clase Enum en sí (y EnumVal dentro de ella):

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

}

Y aquí hay un uso más avanzado que controla los ID y agrega datos / métodos a la abstracción Val y a la enumeración misma:

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)
}
revs AmigoNico
fuente
Tyvm por proporcionar esto. Realmente lo aprecio. Sin embargo, estoy notando que está usando "var" en lugar de val. Y este es un pecado mortal límite en el mundo de la PF. Entonces, ¿hay alguna manera de implementar esto de tal manera que no se use var? Es curioso si se trata de algún tipo de caso límite de tipo FP y no entiendo cómo su implementación es indeseable.
chaotic3quilibrium
2
Probablemente no pueda ayudarte. Es bastante común en Scala escribir clases que mutan internamente pero que son inmutables para quienes las usan. En el ejemplo anterior, un usuario de DayOfWeek no puede mutar la enumeración; no hay forma, por ejemplo, de cambiar la identificación del martes, o su nombre, después del hecho. Pero si desea una implementación que esté libre de mutación internamente , entonces no tengo nada. Sin embargo, no me sorprendería ver una nueva y agradable instalación de enumeración basada en macros en 2.11; Se están dando ideas en scala-lang.
AmigoNico
Recibo un error extraño en Scala Worksheet. Si uso directamente una de las instancias de Value, obtengo un error de inicialización. Sin embargo, si hago una llamada al método .values ​​para ver el contenido de la enumeración, eso funciona y luego funciona directamente usando la instancia de valor. ¿Alguna idea de cuál es el error de inicialización? ¿Y cuál es la forma óptima de garantizar que la inicialización se realice en el orden correcto independientemente de la convención de llamada?
chaotic3quilibrium
@ chaotic3quilibrium: ¡Guau! Gracias por perseguir esto y, por supuesto, gracias a Rex Kerr por el trabajo pesado. Mencionaré el problema aquí y me referiré a la pregunta que creó.
AmigoNico
"[El uso var] es un pecado mortal límite en el mundo de la PF". No creo que la opinión sea universalmente aceptada.
Erik Kaplun
12

Tengo una buena biblioteca simple aquí que le permite usar rasgos / clases selladas como valores de enumeración sin tener que mantener su propia lista de valores. Se basa en una macro simple que no depende del buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum

lloydmeta
fuente
10

Actualización de marzo de 2017: como comentó Anthony Accioly , el scala.Enumeration/enumRP ha sido cerrado.

Dotty (compilador de la próxima generación para Scala) tomará la delantera, aunque en el número de 1970 y en el PR 1958 de Martin Odersky .


Nota: ahora hay (agosto de 2016, más de 6 años después) una propuesta para eliminar scala.Enumeration: PR 5352

Desaprobar scala.Enumeration, agregar @enumanotaciones

La sintaxis

@enum
 class Toggle {
  ON
  OFF
 }

es un posible ejemplo de implementación, la intención es también admitir ADT que cumplan con ciertas restricciones (sin anidamiento, recursión o parámetros variables del constructor), por ejemplo:

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

Deprecia el desastre no mitigado que es scala.Enumeration.

Ventajas de @enum sobre scala. Enumeración:

  • En realidad funciona
  • Interoperabilidad Java
  • No hay problemas de borrado
  • No hay mini-DSL confusas para aprender al definir enumeraciones

Desventajas: ninguna.

Esto soluciona el problema de no poder tener una base de código compatible con Scala-JVM Scala.jsy Scala-Native (el código fuente de Java no es compatible Scala.js/Scala-Native, el código fuente de Scala no puede definir enumeraciones que son aceptadas por las API existentes en Scala-JVM).

VonC
fuente
El PR anterior se ha cerrado (sin alegría). Ahora es 2017 y parece que Dotty finalmente obtendrá una construcción enum. Aquí está el problema y las relaciones públicas de Martin . Fusionar, fusionar, fusionar!
Anthony Accioly
8

Otra desventaja de las clases de casos versus Enumeraciones cuando necesitará iterar o filtrar en todas las instancias. Esta es una capacidad incorporada de Enumeration (y las enumeraciones de Java también), mientras que las clases de casos no admiten automáticamente dicha capacidad.

En otras palabras: "no hay una manera fácil de obtener una lista del conjunto total de valores enumerados con clases de casos".

usuario142435
fuente
5

Si realmente quiere mantener la interoperabilidad con otros lenguajes JVM (por ejemplo, Java), la mejor opción es escribir enumeraciones Java. Esos funcionan de manera transparente tanto desde el código Scala como desde Java, que es más de lo que se puede decir de los scala.Enumerationobjetos de caso. ¡No podemos tener una nueva biblioteca de enumeraciones para cada nuevo proyecto de pasatiempo en GitHub, si se puede evitar!

Connor Doyle
fuente
4

He visto varias versiones de hacer que una clase de caso imite una enumeración. Aquí está mi versión:

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

Lo que le permite construir clases de casos similares a las siguientes:

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

Tal vez alguien podría idear un truco mejor que simplemente agregar una clase de cada caso a la lista como lo hice yo. Esto fue todo lo que se me ocurrió en ese momento.

jho
fuente
¿Por qué dos métodos de aplicación separados aunque?
Saish
@jho He estado tratando de resolver su solución tal cual, pero no se compilará. En el segundo fragmento de código, hay una referencia al Sitio en "tipo V = Sitio". No estoy seguro de a qué se refiere para aclarar el error de compilación. A continuación, ¿por qué proporciona los corchetes vacíos para la "moneda de clase abstracta"? ¿No podrían simplemente quedarse? Finalmente, ¿por qué estás usando una var en "var values ​​= ..."? ¿No significa esto que los clientes podrían en cualquier momento desde cualquier parte del código asignar una nueva Lista a los valores? ¿No sería preferible convertirlo en val en lugar de var?
chaotic3quilibrium
2

He estado yendo y viniendo de estas dos opciones las últimas veces que las he necesitado. Hasta hace poco, mi preferencia ha sido la opción de objeto de rasgo / caso sellado.

1) Declaración de enumeración Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Rasgos Sellados + Objetos de Caso

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Si bien ninguno de estos realmente cumple con todo lo que una enumeración de Java le brinda, a continuación se detallan los pros y los contras:

Enumeración Scala

Pros: -Funciones para crear instancias con opción o asumir directamente la precisión (más fácil cuando se carga desde una tienda persistente) -Se admite la iteración sobre todos los valores posibles

Contras: -La advertencia de compilación para la búsqueda no exhaustiva no es compatible (hace que la coincidencia de patrones sea menos ideal)

Objetos de caso / rasgos sellados

Pros: -Utilizando rasgos sellados, podemos pre-instanciar algunos valores, mientras que otros pueden inyectarse en el momento de la creación -completo soporte para la coincidencia de patrones (métodos de aplicar / no aplicar definidos)

Contras: -Instantando desde una tienda persistente - a menudo tienes que usar la coincidencia de patrones aquí o definir tu propia lista de todos los 'valores de enumeración' posibles

Lo que finalmente me hizo cambiar mi opinión fue algo como el siguiente fragmento:

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
}

Las .getllamadas fueron horribles: en su lugar, usando la enumeración simplemente puedo llamar al método withName en la enumeración de la siguiente manera:

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

Por lo tanto, creo que mi preferencia en el futuro es usar Enumeraciones cuando se pretende acceder a los valores desde un repositorio y objetos de caso / rasgos sellados de lo contrario.

Perro rabioso
fuente
Puedo ver cómo es deseable el segundo patrón de código (deshacerse de los dos métodos auxiliares del primer patrón de código). Sin embargo, descubrí una forma en la que no estás obligado a elegir entre estos dos patrones.
Cubro
2

Prefiero case objects(es una cuestión de preferencia personal). Para hacer frente a los problemas inherentes a ese enfoque (analizar la cadena e iterar sobre todos los elementos), he agregado algunas líneas que no son perfectas, pero son efectivas.

Te estoy pegando el código aquí esperando que pueda ser útil, y también que otros puedan mejorarlo.

/**
 * 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 }
}
jaguililla
fuente
0

Para aquellos que todavía buscan cómo hacer que funcione la respuesta de GatesDa : puede hacer referencia al objeto de caso después de declararlo para instanciarlo:

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
}
V-Lamp
fuente
0

Creo que la mayor ventaja de tener case classesmás enumerationses que puedes usar el patrón de clase de tipo también conocido como polimorfismo ad-hoc . No es necesario que coincida con enumeraciones como:

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

en cambio tendrás algo como:

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() = ...
}
Murat Mustafin
fuente