Saltar al contenido

Objetos de caso vs enumeraciones en Scala

Ya no tienes que investigar más por todo internet ya que has llegado al sitio justo, tenemos la solución que quieres pero sin complicarte.

Solución:

Una gran diferencia es que Enumerations vienen con soporte para instanciarlos de algunos name Cuerda. 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 ser un complemento incómodo, por lo que ahora tiendo a usar case objects. A case object es 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

Así que 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 “Moneda desconocida (código)”, hay otras formas de manejar no encontrar una cadena de código de moneda que “romper” la naturaleza de conjunto cerrado del Currency escribe. UnknownCurrency ser de tipo Currency ahora puede colarse en otras partes de una API.

Es aconsejable sacar ese caso al exterior Enumeration y hacer que el cliente se ocupe de un Option[Currency] tipo que indicaría claramente que realmente existe un problema de coincidencia y “alentaría” al usuario de la API a resolverlo él mismo.

Para dar seguimiento a las otras respuestas aquí, los principales inconvenientes de case objectse acabó Enumerations son:

  1. No se pueden iterar todas las instancias de la “enumeración”. Este es ciertamente el caso, pero he encontrado extremadamente raro en la práctica que esto sea necesario.

  2. No se pueden crear instancias fácilmente a partir 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.

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

Resumen:

Hay tres patrones básicos para intentar reproducir Java Enum dentro de un proyecto Scala. Dos de los tres patrones; directamente usando Java Enum y scala.Enumeration, no son capaces de habilitar la exhaustiva coincidencia de patrones de Scala. Y el tercero; “objeto sellado rasgo + caso”, lo hace … pero tiene complicaciones de inicialización de clase / objeto de JVM que dan como resultado una generación de índice ordinal inconsistente.

He creado una solución con dos clases; Enumeration and EnumerationDecorated, ubicado en este Gist. No publiqué el código en este hilo porque el archivo de Enumeración 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 empezar, asegurémonos de que estamos trabajando desde la misma idea básica de lo que es una enumeración. Definamos una enumeración principalmente en términos de Enum proporcionado a partir de Java 5 (1.5):

  1. Contiene un conjunto cerrado ordenado naturalmente de miembros nombrados
    1. Hay un número fijo de miembros.
    2. Los miembros están naturalmente ordenados e indexados explícitamente
      • A diferencia de ser ordenados en función de algunos criterios de consulta de miembros inactos
    3. Cada miembro tiene un nombre único dentro del conjunto total de todos los miembros.
  2. Todos los miembros se pueden iterar fácilmente en función de sus índices
  3. Un miembro se puede recuperar con su nombre (sensible a mayúsculas y minúsculas)
    1. Sería muy bueno si un miembro también pudiera recuperarse con su nombre que no distingue entre mayúsculas y minúsculas
  4. Un miembro se puede recuperar con su índice
  5. Los miembros pueden usar la serialización de manera fácil, transparente y eficiente
  6. Los miembros pueden extenderse fácilmente para contener datos de unicidad asociados adicionales
  7. Pensando más allá de Java Enum, sería bueno poder aprovechar explícitamente la comprobación exhaustiva de coincidencia de patrones de Scala para una enumeración

A continuación, veamos versiones resumidas de los tres patrones de solución más comunes publicados:

A) Realmente usando directamente Java Enum patrón (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 recuperarse con su nombre que no distingue entre mayúsculas y minúsculas
  2. 7 – Pensando más allá del Enum de Java, sería bueno poder aprovechar explícitamente la exhaustividad de coincidencia de patrones de Scala para verificar una enumeración

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

B) Utilizando el “sealed trait + case objects” patrón:

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 están naturalmente ordenados y explícitamente indexados
  2. 2 – Todos los miembros se pueden iterar 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 recuperarse 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 exagerado afirmar que es eficiente. Para 6, no es realmente fácil extenderlo para contener datos de unicidad asociados adicionales.

C) Utilizando el scala.Enumeration patró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 Java Enum):

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

Nuevamente, para mis proyectos actuales, el elemento 7 es fundamental para permitirme detectar problemas de tiempo de compilación si / cuando agrego / elimino miembros de enumeración o estoy escribiendo un 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.Enumeration

Cada una de estas soluciones puede eventualmente ser reelaborada / expandida / refactorizada para intentar cubrir algunos de los requisitos faltantes de cada uno. Sin embargo, ni Java Enum ni el scala.Enumeration las soluciones se pueden expandir lo suficiente para proporcionar el elemento 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 una brecha / problema en mi código en lugar de tener que extraerlo de una excepción / falla en tiempo de ejecución de producción.


En ese sentido, me puse a trabajar con el case object camino para ver si puedo producir una solución que cubra toda la definición de enumeración anterior. El primer desafío fue avanzar en el núcleo del problema de inicialización de clase / objeto de 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 desde el Enumeration El rasgo tiene más de 400 líneas de largo (muchos comentarios que explican el contexto), estoy renunciando a pegarlo en este hilo (lo que haría que se extendiera considerablemente hacia abajo en la página). Para obtener más información, vaya directamente a la esencia.

Así es como termina la solución usando la misma idea de datos que la anterior (versión completamente 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é (ubicados 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 (decorationOrderedSet en 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 de JVM para este modelo de objeto de objeto / caso en particular no está definida (consulte este hilo de Stackoverflow)
  2. El contenido devuelto por el método. getClass.getDeclaredClasses tiene un orden indefinido (y es poco probable que esté en el mismo orden que el case object declaraciones en el código fuente)

Dados estos dos problemas, tuve que dejar de intentar generar un orden implícito y tuve que exigir explícitamente que el cliente lo definiera y lo declarara con algún tipo de noción de conjunto ordenado. Como las colecciones de Scala no tienen una implementación de conjunto ordenado de inserción, lo mejor que pude hacer fue usar un List y luego verifique en tiempo de ejecución que realmente fue un conjunto. No es como hubiera preferido haber logrado esto.

Y dado que el diseño requería esta segunda lista / pedido de conjuntos val, Dado que ChessPiecesEnhancedDecorated ejemplo anterior, fue posible agregar case object PAWN2 extends Member y luego olvídate de agregar Decoration(PAWN2,'P2', 2) para decorationOrderedSet. Por lo tanto, hay una verificación de tiempo de ejecución para verificar que la lista no es solo un conjunto, sino que contiene TODOS los objetos de casos que extienden el sealed trait Member. Esa fue una forma especial de reflexión / macroinfierno para trabajar.

Deje comentarios y / o comentarios sobre la esencia.

Los objetos de caso ya devuelven su nombre para sus métodos toString, por lo que pasarlo por separado no es necesario. 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 perezosos; al usar vals en su lugar, podemos eliminar 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 trampas, puede precargar sus valores de enumeración usando la API de reflexión o algo como Reflejos de Google. Los objetos de mayúsculas y minúsculas no perezosos le brindan 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 Scala idiomático código:

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

Eres capaz de añadir valor a nuestro contenido aportando tu experiencia en las notas.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *