Objetos de caso x enumerações no Scala

231

Existem diretrizes de boas práticas sobre quando usar classes de caso (ou objetos de caso) versus estender a enumeração no Scala?

Eles parecem oferecer alguns dos mesmos benefícios.

Alex Miller
fonte
2
Eu escrevi uma pequena visão geral sobre scala enumeração e alternativas, você pode achar que é útil: pedrorijo.com/blog/scala-enums/
pedrorijo91
1
Veja também o Scala 3 baseado em Dottyenum (para meados de 2020).
VonC 12/06/19

Respostas:

223

Uma grande diferença é que eles Enumerationvêm com suporte para instancia-los a partir de alguma nameString. Por exemplo:

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

Então você pode fazer:

val ccy = Currency.withName("EUR")

Isso é útil quando se deseja persistir enumerações (por exemplo, em um banco de dados) ou criá-las a partir de dados residentes em arquivos. No entanto, acho que, em geral, as enumerações são um pouco desajeitadas no Scala e têm a sensação de um complemento estranho, então agora tenho a tendência de usar case objects. A case objecté mais flexível que um enum:

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

case class UnknownCurrency(name: String) extends Currency

Então agora eu tenho a vantagem de ...

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

Como o @ chaotic3quilibrium apontou (com algumas correções para facilitar a leitura):

Em relação ao padrão "UnknownCurrency (code)", há outras maneiras de lidar com a não localização de uma sequência de códigos de moeda além de "quebrar" a natureza do conjunto fechado do Currencytipo. UnknownCurrencyser do tipo Currencyagora pode se infiltrar em outras partes de uma API.

É aconselhável levar esse caso para fora Enumeratione fazer com que o cliente lide com um Option[Currency]tipo que indique claramente que há realmente um problema de correspondência e "incentive" o usuário da API a resolver por conta própria.

Para acompanhar as outras respostas aqui, as principais desvantagens de case objects sobre Enumerations são:

  1. Não é possível iterar em todas as instâncias da "enumeração" . Certamente é esse o caso, mas achei extremamente raro na prática que isso seja necessário.

  2. Não é possível instanciar facilmente a partir do valor persistente . Isso também é verdade, mas, exceto no caso de grandes enumerações (por exemplo, todas as moedas), isso não apresenta uma sobrecarga enorme.

oxbow_lakes
fonte
10
A outra diferença é que a enumeração de enumeração é ordenado fora da caixa, enquanto que caso objeto enum baseado obviosly não
om-nom-nom
1
Outro ponto para objetos de caso é se você se preocupa com a interoperabilidade java. A Enumeração retornaria os valores como Enumeration.Value, portanto 1) exigindo scala-library, 2) perdendo as informações reais do tipo.
Juanmirocks #
7
@oxbow_lakes Em relação ao ponto 1, especificamente esta parte "... eu achei extremamente raro na prática que isso seja necessário": aparentemente você raramente faz muito trabalho na interface do usuário. Este é um caso de uso extremamente comum; exibindo uma lista (suspensa) de membros de enumeração válidos dos quais escolher.
chaotic3quilibrium 19/09/14
Não entendo o tipo de item que está sendo correspondido trade.ccyno exemplo de característica selada.
Rloth 26/01
e não case objectgera uma pegada de código maior (~ 4x) que Enumeration? Distinção útil, especialmente para scala.jsprojetos que necessitam de uma pegada reduzida.
eCoE
69

ATUALIZAÇÃO: Uma nova solução baseada em macro foi criada, que é muito superior à solução descrita abaixo. Eu recomendo fortemente o uso desta nova solução baseada em macro . E parece que os planos para o Dotty farão com que esse estilo de solução enum faça parte do idioma. Whoohoo!

Resumo:
Existem três padrões básicos para tentar reproduzir o Java Enumem um projeto Scala. Dois dos três padrões; diretamente usando Java Enume scala.Enumeration, não são capazes de ativar a correspondência exaustiva de padrões do Scala. E o terceiro; "traço selado + objeto de caso", possui ... mas possui complicações de inicialização de classe / objeto da JVM, resultando em geração inconsistente de índice ordinal.

Eu criei uma solução com duas classes; Enumeração e EnumerationDecorated , localizado nesta Gist . Não publiquei o código nesse segmento, pois o arquivo para Enumeração era bastante grande (+400 linhas - contém muitos comentários explicando o contexto de implementação).

Detalhes:
a pergunta que você está fazendo é bem geral; "... quando usar caseclassesobjects versus extensão [scala.]Enumeration". E acontece que existem MUITAS respostas possíveis, cada resposta dependendo das sutilezas dos requisitos específicos do projeto que você possui. A resposta pode ser reduzida para três padrões básicos.

Para começar, vamos ter certeza de que estamos trabalhando com a mesma idéia básica do que é uma enumeração. Vamos definir uma enumeração principalmente em termos do Enumfornecido no Java 5 (1.5) :

  1. Ele contém um conjunto fechado ordenado naturalmente de membros nomeados
    1. Há um número fixo de membros
    2. Os membros são naturalmente ordenados e explicitamente indexados
      • Em vez de ser classificado com base em alguns critérios questionáveis ​​de membros inatos
    3. Cada membro tem um nome exclusivo dentro do conjunto total de todos os membros
  2. Todos os membros podem ser facilmente iterados com base em seus índices
  3. Um membro pode ser recuperado com seu nome (diferencia maiúsculas de minúsculas)
    1. Seria ótimo se um membro também pudesse ser recuperado com seu nome que não diferencia maiúsculas de minúsculas
  4. Um membro pode ser recuperado com seu índice
  5. Os membros podem usar de forma fácil, transparente e eficiente a serialização
  6. Os membros podem ser facilmente estendidos para armazenar dados adicionais associados a singleton
  7. Pensando além do Java Enum, seria bom poder aproveitar explicitamente a verificação exaustiva de correspondência de padrões do Scala para uma enumeração

A seguir, vejamos as versões resumidas dos três padrões de solução mais comuns publicados:

A) Na verdade, diretamente usando o padrão JavaEnum (em um projeto Scala / Java misto):

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

Os seguintes itens da definição de enumeração não estão disponíveis:

  1. 3.1 - Seria ótimo se um membro também pudesse ser recuperado com seu nome que não diferencia maiúsculas de minúsculas
  2. 7 - Pensando além do Enum do Java, seria bom poder aproveitar explicitamente a verificação exaustiva da correspondência de padrões do Scala para uma enumeração

Para meus projetos atuais, não tenho o benefício de correr riscos ao longo do caminho misto do projeto Scala / Java. E mesmo que eu pudesse optar por fazer um projeto misto, o item 7 é essencial para permitir que eu pegue problemas de tempo de compilação se / quando adiciono / removo membros da enumeração ou estou escrevendo algum novo código para lidar com os membros existentes da enumeração.


B) Usando o padrão " 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}
}

Os seguintes itens da definição de enumeração não estão disponíveis:

  1. 1.2 - Membros são ordenados naturalmente e indexados explicitamente
  2. 2 - Todos os membros podem ser facilmente iterados com base em seus índices
  3. 3 - Um membro pode ser recuperado com seu nome (diferencia maiúsculas de minúsculas)
  4. 3.1 - Seria ótimo se um membro também pudesse ser recuperado com seu nome que não diferencia maiúsculas de minúsculas
  5. 4 - Um membro pode ser recuperado com seu índice

É discutível que ele realmente atenda aos itens de definição de enumeração 5 e 6. Para 5, é muito difícil afirmar que é eficiente. Para 6, não é realmente fácil estender para armazenar dados adicionais associados a singleton.


C) Usando o scala.Enumerationpadrão (inspirado nesta resposta do 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]
}

Os seguintes itens da definição de enumeração não estão disponíveis (é idêntico à lista para usar diretamente o Java Enum):

  1. 3.1 - Seria ótimo se um membro também pudesse ser recuperado com seu nome que não diferencia maiúsculas de minúsculas
  2. 7 - Pensando além do Enum do Java, seria bom poder aproveitar explicitamente a verificação exaustiva da correspondência de padrões do Scala para uma enumeração

Novamente para meus projetos atuais, o item 7 é fundamental para permitir que eu pegue problemas de tempo de compilação se / quando adiciono / removo membros da enumeração ou estou escrevendo algum novo código para lidar com os membros existentes da enumeração.


Portanto, dada a definição acima de uma enumeração, nenhuma das três soluções acima funciona, pois elas não fornecem tudo descrito na definição de enumeração acima:

  1. Java Enum diretamente em um projeto Scala / Java misto
  2. "característica selada + objetos de caso"
  3. scala.Enumeration

Cada uma dessas soluções pode ser retrabalhada / expandida / refatorada para tentar cobrir alguns dos requisitos ausentes de cada um. No entanto, nem o Java Enumnem as scala.Enumerationsoluções podem ser suficientemente expandidas para fornecer o item 7. E para meus próprios projetos, esse é um dos valores mais convincentes do uso de um tipo fechado no Scala. Eu prefiro fortemente avisos / erros de tempo de compilação para indicar que tenho uma lacuna / problema no meu código, em vez de precisar coletá-lo de uma exceção / falha no tempo de execução da produção.


Nesse sentido, comecei a trabalhar com o case objectcaminho para ver se conseguia produzir uma solução que cobrisse toda a definição de enumeração acima. O primeiro desafio foi avançar no núcleo do problema de inicialização de classe / objeto da JVM (abordado em detalhes nesta postagem do StackOverflow ). E finalmente consegui descobrir uma solução.

Como minha solução são duas características; Enumeration and EnumerationDecorated , e como a Enumerationcaracterística tem mais de 400 linhas (muitos comentários explicando o contexto), deixo de colar o texto nesse segmento (o que o faria esticar a página consideravelmente). Para detalhes, pule diretamente para o Gist .

Eis como a solução acaba usando a mesma ideia de dados acima (versão totalmente comentada disponível aqui ) e implementada no 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 é um exemplo de uso de um novo par de traços de enumeração que eu criei (localizado nesta lista ) para implementar todos os recursos desejados e descritos na definição de enumeração.

Uma preocupação expressa é que os nomes dos membros da enumeração devem ser repetidos ( decorationOrderedSetno exemplo acima). Embora eu o minimizasse até uma única repetição, não conseguia ver como torná-lo ainda menos devido a dois problemas:

  1. A inicialização de objeto / classe da JVM para este modelo de objeto / caso é indefinida (consulte este encadeamento Stackoverflow )
  2. O conteúdo retornado do método getClass.getDeclaredClassestem uma ordem indefinida (e é improvável que esteja na mesma ordem que as case objectdeclarações no código-fonte)

Dadas essas duas questões, tive que desistir de tentar gerar uma ordem implícita e exigir explicitamente que o cliente a definisse e a declarasse com algum tipo de noção de conjunto ordenada. Como as coleções Scala não possuem uma implementação de conjunto ordenada por inserção, o melhor que pude fazer foi usar uma Listverificação de tempo de execução e, em seguida, verificar se realmente era um conjunto. Não é assim que eu teria preferido ter conseguido isso.

E dado o design exigido para a segunda ordem de lista / conjunto val, conforme o ChessPiecesEnhancedDecoratedexemplo acima, foi possível adicionar case object PAWN2 extends Membere depois esquecer de adicionar Decoration(PAWN2,'P2', 2)a decorationOrderedSet. Portanto, há uma verificação de tempo de execução para verificar se a lista não é apenas um conjunto, mas contém TODOS os objetos de caso que estendem o arquivo sealed trait Member. Essa foi uma forma especial de reflexão / macro inferno para resolver.


Por favor, deixe comentários e / ou feedback sobre o Gist .

chaotic3quilibrium
fonte
Eu já lançou a primeira versão da biblioteca ScalaOlio (GPLv3), que contém versões mais up-to-date de ambos org.scalaolio.util.Enumeratione org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium
E para saltar diretamente para o repositório ScalaOlio no Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium
5
Esta é uma resposta de qualidade e muito a ser tirada. Obrigado
angabriel
1
Parece que Odersky está querendo atualizar o Dotty (futuro Scala 3.0) com um enum nativo. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium
62

Objetos de caso já retornam seus nomes para seus métodos toString, portanto, passá-los separadamente é desnecessário. Aqui está uma versão semelhante à do jho (métodos de conveniência omitidos por questões de brevidade):

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

Objetos são preguiçosos; usando vals, podemos descartar a lista, mas precisamos repetir o nome:

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

Se você não se importa com trapaças, pode pré-carregar seus valores de enumeração usando a API de reflexão ou algo como o Google Reflections. Objetos de caso não preguiçosos oferecem a sintaxe mais limpa:

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
}

Agradável e limpo, com todas as vantagens de classes de casos e enumerações Java. Pessoalmente, defino os valores de enumeração fora do objeto para corresponder melhor ao código idiomático do Scala:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
GatesDA
fonte
3
uma pergunta: a última solução é chamada de "objetos de caso não preguiçosos", mas, neste caso, os objetos não são carregados até usá-los: por que você chama essa solução de não preguiçoso?
Seb Cesbron
2
@Noel, você precisa usar: paste para colar toda a hierarquia selada no REPL. Caso contrário, a linha única com a classe / característica base selada conta como um único arquivo, é selada imediatamente e não pode ser estendida na próxima linha.
Jürgen Strobel
2
@GatesDA Apenas seu primeiro trecho de código não possui um bug (como você exige explicitamente que o cliente declare e defina valores. Suas segunda e terceira soluções têm o bug sutil que eu descrevi no meu último comentário (se o cliente acessar a moeda .GBP diretamente e primeiro, os valores da Lista "estarão fora de ordem"). Eu explorei o domínio de enumeração Scala extensivamente e o cobrei em detalhes na minha resposta para esse mesmo segmento: stackoverflow.com/a/25923651/501113
chaotic3quilibrium 19/09/2014
1
Talvez uma das desvantagens dessa abordagem (em comparação com as Java Enums de qualquer maneira) seja que, quando você digita Currency <dot> no IDE, ele não mostra as opções disponíveis.
Ivan Balashov 02/03
1
Como o @SebCesbron mencionou, os objetos do caso são preguiçosos aqui. Portanto, se eu ligar Currency.values, só recebo de volta os valores acessados ​​anteriormente. Existe alguma maneira de contornar isso?
Sasgorilla 9/02/19
27

As vantagens de usar classes de caso sobre enumerações são:

  • Ao usar classes de caso seladas, o compilador Scala pode dizer se a correspondência está totalmente especificada, por exemplo, quando todas as correspondências possíveis são defendidas na declaração de correspondência. Com enumerações, o compilador Scala não pode dizer.
  • As classes de caso suportam naturalmente mais campos do que uma enumeração baseada em valor que suporta um nome e um ID.

As vantagens de usar enumerações em vez de classes de caso são:

  • As enumerações geralmente terão um pouco menos de código para escrever.
  • As enumerações são um pouco mais fáceis de entender para alguém novo no Scala, pois elas prevalecem em outros idiomas

Portanto, em geral, se você só precisa de uma lista de constantes simples por nome, use enumerações. Caso contrário, se você precisar de algo um pouco mais complexo ou desejar a segurança extra do compilador, informando se todas as correspondências foram especificadas, use classes de casos.

Aaron
fonte
15

ATUALIZAÇÃO: O código abaixo possui um erro, descrito aqui . O programa de teste abaixo funciona, mas se você usasse o DayOfWeek.Mon (por exemplo) antes do DayOfWeek, ele falharia porque o DayOfWeek não foi inicializado (o uso de um objeto interno não faz com que um objeto externo seja inicializado). Você ainda pode usar esse código se fizer algo como val enums = Seq( DayOfWeek )na sua classe principal, forçando a inicialização de suas enumerações ou usar as modificações do caotic3quilibrium. Ansioso para um enum baseado em macro!


Se você quiser

  • avisos sobre correspondências não exaustivas de padrões
  • um ID Int atribuído a cada valor de enum, que você pode opcionalmente controlar
  • uma lista imutável dos valores enum, na ordem em que foram definidos
  • um mapa imutável do nome ao valor da enumeração
  • um mapa imutável do valor de id para enum
  • locais para colar métodos / dados para todos ou valores de enum específicos ou para o enum como um todo
  • valores de enumeração ordenados (para que você possa testar, por exemplo, se dia <quarta-feira)
  • a capacidade de estender um enum para criar outros

então o seguinte pode ser de seu interesse. Feedback bem-vindo.

Nesta implementação, existem classes básicas abstratas Enum e EnumVal, que você estende. Veremos essas classes em um minuto, mas primeiro, veja como você definiria uma enumeração:

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

Observe que você precisa usar cada valor de enum (chame seu método de aplicação) para dar vida a ele. [Eu gostaria que os objetos internos não fossem preguiçosos, a menos que eu pedisse especificamente que eles fossem. Eu acho que.]

É claro que poderíamos adicionar métodos / dados ao DayOfWeek, Val ou aos objetos de caso individuais, se assim o desejássemos.

E aqui está como você usaria essa enumeração:

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

}

Aqui está o que você obtém quando o compila:

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

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

Você pode substituir "correspondência do dia" por "correspondência do dia: @unchecked)" onde não deseja esses avisos ou simplesmente incluir um caso abrangente no final.

Quando você executa o programa acima, obtém esta saída:

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

Observe que, como a Lista e os Mapas são imutáveis, você pode remover facilmente elementos para criar subconjuntos, sem interromper a enumeração em si.

Aqui está a própria classe Enum (e EnumVal dentro dela):

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

}

E aqui está um uso mais avançado dele que controla os IDs e adiciona dados / métodos à abstração Val e à própria enumeração:

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)
}
AmigoNico
fonte
Tyvm por fornecer isso. Eu realmente gostei disso. No entanto, estou percebendo que ele está usando "var" em vez de val. E este é um pecado mortal limítrofe no mundo das PF. Então, existe uma maneira de implementar isso de forma que não haja uso de var? Apenas curioso se isso é algum tipo de caso de borda do tipo FP e não estou entendendo como sua implementação é FP indesejável.
chaotic3quilibrium
2
Eu provavelmente não posso te ajudar. É bastante comum em Scala escrever classes que sofrem mutações internamente, mas que são imutáveis ​​para quem as utiliza. No exemplo acima, um usuário do DayOfWeek não pode alterar a enumeração; não há como, por exemplo, alterar o ID da terça-feira, ou seu nome, após o fato. Mas se você quer uma implementação livre de mutações internamente , não tenho nada. Eu não ficaria surpreso ao ver um novo e agradável recurso de enum baseado em macros na versão 2.11; idéias estão sendo lançadas em scala-lang.
AmigoNico 19/01
Estou recebendo um erro estranho na planilha Scala. Se eu usar diretamente uma das instâncias Value, recebo um erro de inicialização. No entanto, se eu fizer uma chamada para o método .values ​​para ver o conteúdo da enumeração, isso funcionará e, em seguida, diretamente usando a instância value funcionará. Alguma idéia de qual é o erro de inicialização? E qual é a melhor maneira de garantir que a inicialização ocorra na ordem correta, independentemente da convenção de chamada?
chaotic3quilibrium
@ chaotic3quilibrium: Uau! Obrigado por prosseguir e, claro, obrigado a Rex Kerr pelo trabalho pesado. Vou mencionar o problema aqui e me referir à pergunta que você criou.
AmigoNico 19/02
"[Usar var] é um pecado mortal limítrofe no mundo das PF" - não acho que essa opinião seja universalmente aceita.
precisa
12

Eu tenho uma boa e simples lib aqui que permite que você use características / classes seladas como valores de enumeração sem precisar manter sua própria lista de valores. Ele se baseia em uma macro simples que não depende do buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum

lloydmeta
fonte
10

Atualização em março de 2017: conforme comentado por Anthony Accioly , o scala.Enumeration/enumPR foi fechado.

Dotty (compilador de próxima geração para Scala) assumirá a liderança, embora a edição dotty de 1970 e o PR 1958 de Martin Odersky .


Nota: existe agora (agosto de 2016, mais de 6 anos depois) uma proposta para remover scala.Enumeration: PR 5352

Descontinuar scala.Enumeration, adicionar @enumanotação

A sintaxe

@enum
 class Toggle {
  ON
  OFF
 }

é um possível exemplo de implementação, a intenção é também oferecer suporte a ADTs que estejam em conformidade com certas restrições (sem aninhamento, recursão ou variação de parâmetros do construtor), por exemplo:

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

Preteri o desastre não mitigado que é scala.Enumeration.

Vantagens do @enum sobre o scala.

  • Realmente funciona
  • Interoperabilidade Java
  • Sem problemas de apagamento
  • Sem mini-DSL confuso para aprender ao definir enumerações

Desvantagens: Nenhuma.

Isso resolve o problema de não poder ter uma base de código que suporte Scala-JVM Scala.jse Scala-Native (código-fonte Java não suportado Scala.js/Scala-Native, código-fonte Scala não é capaz de definir enumerações que são aceitas pelas APIs existentes no Scala-JVM).

VonC
fonte
O PR acima foi fechado (sem alegria). Agora é 2017 e parece que Dotty finalmente vai ter uma construção enum. Aqui está a questão e o PR de Martin . Mesclar, mesclar, mesclar!
Anthony Accioly
8

Outra desvantagem de classes de caso versus enumerações, quando você precisará iterar ou filtrar todas as instâncias. Esse é um recurso interno da Enumeração (e também das enumerações Java), enquanto as classes de caso não suportam automaticamente esse recurso.

Em outras palavras: "não há uma maneira fácil de obter uma lista do conjunto total de valores enumerados com classes de caso".

user142435
fonte
5

Se você é sério em manter a interoperabilidade com outras linguagens da JVM (por exemplo, Java), a melhor opção é escrever enumerações Java. Eles funcionam de forma transparente a partir dos códigos Scala e Java, o que é mais do que pode ser dito para scala.Enumerationobjetos ou caso. Não vamos ter uma nova biblioteca de enumerações para cada novo projeto de hobby no GitHub, se puder ser evitado!

Connor Doyle
fonte
4

Eu já vi várias versões de fazer uma classe de caso imitar uma enumeração. Aqui está a minha versão:

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

O que permite construir classes de caso que se parecem com o seguinte:

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

Talvez alguém possa inventar um truque melhor do que simplesmente adicionar uma classe de cada caso à lista, como eu fiz. Isso foi tudo o que pude apresentar na época.

jho
fonte
Por que dois métodos separados de não aplicação?
Saish
@ jho Estou tentando trabalhar com sua solução como está, mas ela não será compilada. No segundo trecho de código, há uma referência ao site em "tipo V = site". Não sei ao que isso se refere para esclarecer o erro de compilação. Em seguida, por que você está fornecendo os colchetes vazios para a "classe abstrata Moeda"? Eles não poderiam simplesmente ficar de fora? Finalmente, por que você está usando um var em "var values ​​= ..."? Isso não significa que os clientes possam, a qualquer momento e em qualquer lugar do código, atribuir uma nova lista aos valores? Não seria muito preferível torná-lo um val em vez de um var?
chaotic3quilibrium 19/09/14
2

Estive indo e voltando nessas duas opções nas últimas vezes em que precisei delas. Até recentemente, minha preferência era pela opção de característica / objeto de caso selado.

1) Declaração de Enumeração Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Traços selados + objetos de caso

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Embora nenhum deles realmente atenda a tudo o que uma enumeração java oferece, abaixo estão os prós e contras:

Enumeração Scala

Prós: -Funções para instanciar com opção ou assumir diretamente precisão (mais fácil ao carregar de um armazenamento persistente) -Iteração sobre todos os valores possíveis

Contras: -O aviso de compilação para pesquisa não exaustiva não é suportado (torna a correspondência de padrões menos ideal)

Objetos de caso / traços selados

Prós: -Usando traços selados, podemos pré-instanciar alguns valores, enquanto outros podem ser injetados no momento da criação -suporte completo para correspondência de padrões (métodos de aplicação / não aplicação definidos)

Contras: -Instanciando de um armazenamento persistente - você geralmente precisa usar a correspondência de padrões aqui ou definir sua própria lista de todos os possíveis 'valores de enumeração'

O que finalmente me fez mudar de opinião foi algo como o seguinte trecho:

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
}

As .getchamadas eram hediondas - usando enumeração, em vez disso, posso simplesmente chamar o método withName na enumeração da seguinte maneira:

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

Portanto, acho que minha preferência daqui para frente é usar Enumerações quando os valores tiverem a intenção de serem acessados ​​de um repositório e objetos de caso / traços fechados, caso contrário.

Cachorro Louco
fonte
Eu posso ver como o segundo padrão de código é desejável (livrar-se dos dois métodos auxiliares do primeiro padrão de código). No entanto, descobri uma maneira de não ser forçado a escolher entre esses dois padrões. Eu cobrir todo o domínio na resposta Tenho postado para este tópico: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
2

Eu prefiro case objects(é uma questão de preferência pessoal). Para lidar com os problemas inerentes a essa abordagem (analisar a sequência e repetir todos os elementos), adicionei algumas linhas que não são perfeitas, mas são eficazes.

Estou colando o código aqui, esperando que possa ser útil e também que outros possam aprimorá-lo.

/**
 * 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
fonte
0

Para aqueles que ainda desejam obter a resposta do GatesDa para o trabalho : você pode apenas fazer referência ao objeto de caso após declará-lo para instanciar:

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
fonte
0

Eu acho que a maior vantagem de ter case classesmais enumerationsé que você pode usar o padrão de classe de tipo, também conhecido como polimorfismo ad-hoc . Não precisa corresponder enumerações como:

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

em vez disso, você terá 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
fonte