Como modelar tipos enum seguros para tipos?

311

O Scala não possui sistemas seguros enumcomo o Java. Dado um conjunto de constantes relacionadas, qual seria a melhor maneira em Scala de representar essas constantes?

Jesper
fonte
2
Por que não usar apenas java enum? Essa é uma das poucas coisas que ainda prefiro usar java comum.
Max
1
Eu escrevi uma pequena visão geral sobre scala enumeração e alternativas, você pode achar que é útil: pedrorijo.com/blog/scala-enums/
pedrorijo91

Respostas:

187

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

Exemplo de uso

  object Main extends App {

    object WeekDay extends Enumeration {
      type WeekDay = Value
      val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
    }
    import WeekDay._

    def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)

    WeekDay.values filter isWorkingDay foreach println
  }
skaffman
fonte
2
Sério, o aplicativo não deve ser usado. NÃO foi consertado; foi introduzida uma nova classe, App, que não apresenta os problemas mencionados por Schildmeijer. O mesmo com "object foo extends App {...}" E você tem acesso imediato aos argumentos da linha de comando através da variável args.
AmigoNico 25/07/12
scala.Enumeration (que é o que você está usando no seu exemplo de código "objeto WeekDay" acima) não oferece correspondência exaustiva de padrões. Pesquisei todos os diferentes padrões de enumeração atualmente em uso no Scala e forneço uma visão geral deles nesta resposta do StackOverflow (incluindo um novo padrão que oferece o melhor do scala.Enumeration e o padrão "traço selado + objeto de caso": stackoverflow. com / a / 25923651/501113
chaotic3quilibrium
377

Devo dizer que o exemplo copiado da documentação do Scala por skaffman acima é de utilidade limitada na prática (você também pode usar case objects).

Para obter algo parecido com um Java Enum(ou seja, com métodos toStringe valueOfmétodos sensíveis - talvez você esteja mantendo os valores de enum em um banco de dados), é necessário modificá-lo um pouco. Se você usou o código do skaffman :

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

Considerando que usando a seguinte declaração:

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

Você obtém resultados mais sensatos:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
fonte
7
Btw. Agora o método valueOf está morto :-(
greenoldman 23/11
36
valueOfA substituição de @macias é withName, que não retorna uma opção e gera um NSE se não houver correspondência. O que!
Bluu 31/01
6
@Bluu Você pode adicionar valueOf você mesmo: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) para ter uma opção
centr
@centr Quando tento criar um Map[Weekday.Weekday, Long]e adicionar um valor, digamos Monque o compilador lança um erro de tipo inválido. Weekday.Weekday esperado Valor encontrado? Por que isso acontece?
Sohaib 14/06
@Shahaib Deve ser o mapa [Weekday.Value, Long].
centr 16/06
99

Existem muitas maneiras de fazer.

1) Use símbolos. No entanto, ele não fornecerá nenhum tipo de segurança, além de não aceitar não-símbolos, onde um símbolo é esperado. Só estou mencionando aqui por exaustividade. Aqui está um exemplo de uso:

def update(what: Symbol, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case 'row => replaceRow(where, newValue)
    case 'col | 'column => replaceCol(where, newValue)
    case _ => throw new IllegalArgumentException
  }

// At REPL:   
scala> val a = unitMatrixInt(3)
a: teste7.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a('row, 1) = a.row(0)
res41: teste7.MatrixInt =
/ 1 0 0 \
| 1 0 0 |
\ 0 0 1 /

scala> a('column, 2) = a.row(0)
res42: teste7.MatrixInt =
/ 1 0 1 \
| 0 1 0 |
\ 0 0 0 /

2) Usando classe Enumeration:

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

ou, se você precisar serializar ou exibi-lo:

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

Isso pode ser usado assim:

def update(what: Dimension, where: Int, newValue: Array[Int]): MatrixInt =
  what match {
    case Row => replaceRow(where, newValue)
    case Column => replaceCol(where, newValue)
  }

// At REPL:
scala> a(Row, 2) = a.row(1)
<console>:13: error: not found: value Row
       a(Row, 2) = a.row(1)
         ^

scala> a(Dimension.Row, 2) = a.row(1)
res1: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

scala> import Dimension._
import Dimension._

scala> a(Row, 2) = a.row(1)
res2: teste.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 1 0 /

Infelizmente, isso não garante que todas as correspondências sejam contabilizadas. Se eu esquecesse de colocar Row ou Column na partida, o compilador Scala não teria me avisado. Isso me dá algum tipo de segurança, mas não tanto quanto se pode ganhar.

3) Objetos de caso:

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

Agora, se eu deixar de fora um caso em a match, o compilador me avisará:

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

    what match {
    ^
one warning found

É usado praticamente da mesma maneira e nem precisa de import:

scala> val a = unitMatrixInt(3)
a: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 0 0 1 /

scala> a(Row,2) = a.row(0)
res15: teste3.MatrixInt =
/ 1 0 0 \
| 0 1 0 |
\ 1 0 0 /

Você pode se perguntar, então, por que alguma vez usar uma Enumeração em vez de objetos de caso. Por uma questão de fato, os objetos de caso têm vantagens muitas vezes, como aqui. A classe Enumeration, no entanto, possui muitos métodos de coleção, como elementos (iterador no Scala 2.8), que retornam um iterador, mapa, flatMap, filtro etc.

Esta resposta é essencialmente uma parte selecionada deste artigo no meu blog.

Daniel C. Sobral
fonte
"... não aceitando não-símbolos onde um símbolo é esperado"> Suponho que você queira dizer que Symbolinstâncias não podem ter espaços ou caracteres especiais. A maioria das pessoas quando se encontra com a Symbolclasse provavelmente pensa assim, mas na verdade está incorreta. Symbol("foo !% bar -* baz")compila e funciona perfeitamente bem. Em outras palavras, você pode criar perfeitamente Symbolinstâncias envolvendo qualquer string (você simplesmente não pode fazê-lo com o açúcar sintático "coma único"). A única coisa que Symbolgarante é a singularidade de qualquer símbolo, tornando marginalmente mais rápido comparar e comparar.
Régis Jean-Gilles
@ RégisJean-Gilles Não, quero dizer que você não pode passar um String, por exemplo, como argumento para um Symbolparâmetro.
Daniel C. Sobral
Sim, eu entendi essa parte, mas é um ponto discutível se você substituir Stringpor outra classe que é basicamente um invólucro em torno de uma string e pode ser livremente convertida em ambas as direções (como é o caso Symbol). Eu acho que foi isso que você quis dizer ao dizer "Não lhe dará nenhum tipo de segurança", mas não ficou muito claro, dado que o OP solicitou explicitamente soluções seguras para o tipo. Eu não tinha certeza se, no momento em que escrevi, você sabia que não só não é seguro quanto ao tipo, porque essas não são enumerações, mas também Symbol nem garantem que o argumento passado não tenha caracteres especiais.
Régis Jean-Gilles
1
Para elaborar, quando você diz "não aceitar não-símbolos onde um símbolo é esperado", ele pode ser lido como "não aceitando valores que não são instâncias do Symbol" (o que é obviamente verdadeiro) ou "não aceitando valores que não são" identificador-like simples cordas, aka 'símbolos'"(que não é verdade, e é um equívoco que praticamente ninguém tem a primeira vez que encontramos símbolos Scala, devido ao fato de que o primeiro encontro é que o especial 'foonotação que faz obstam cadeias não identificadoras). Esse é um equívoco que eu queria dissipar para qualquer futuro leitor.
Régis Jean-Gilles
@ RégisJean-Gilles eu quis dizer o primeiro, o que é obviamente verdadeiro. Quero dizer, obviamente é verdade para quem está acostumado a digitar estática. Naquela época, havia muita discussão sobre os méritos relativos da digitação estática e "dinâmica", e muitas pessoas interessadas no Scala vinham de um background de digitação dinâmico, então achei que não era preciso dizer isso. Eu nem pensaria em fazer esse comentário hoje em dia. Pessoalmente, acho que o símbolo de Scala é feio e redundante, e nunca o uso. Voto seu último comentário, já que é um bom argumento.
Daniel C. Sobral
52

Uma maneira um pouco menos detalhada de declarar enumerações nomeadas:

object WeekDay extends Enumeration("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") {
  type WeekDay = Value
  val Sun, Mon, Tue, Wed, Thu, Fri, Sat = Value
}

WeekDay.valueOf("Wed") // returns Some(Wed)
WeekDay.Fri.toString   // returns Fri

Obviamente, o problema aqui é que você precisará manter a ordem dos nomes e vals sincronizados, o que é mais fácil se os nomes e val forem declarados na mesma linha.

Walter Chang
fonte
11
Isso parece mais limpo à primeira vista, mas tem a desvantagem de exigir que o mantenedor mantenha o oder de ambas as listas em sincronia. Para o exemplo dos dias da semana, não parece provável. Mas, em geral, o novo valor pode ser inserido ou um excluído e as duas listas podem estar fora de sincronia; nesse caso, erros sutis podem ser introduzidos.
Brent Faust
1
Pelo comentário anterior, o risco é que as duas listas diferentes podem silenciosamente ficar fora de sincronia. Embora não seja um problema para o seu pequeno exemplo atual, se houver muito mais membros (como dezenas ou centenas), as chances das duas listas ficarem silenciosamente fora de sincronia são substancialmente mais altas. O scala.Enumeration também não pode se beneficiar dos avisos / erros exaustivos do padrão de tempo de compilação do Scala. Eu criei uma resposta StackOverflow que contém uma solução realizando uma verificação de tempo de execução para garantir as duas listas permanecem em sincronia: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Você pode usar uma classe abstrata selada em vez da enumeração, por exemplo:

sealed abstract class Constraint(val name: String, val verifier: Int => Boolean)

case object NotTooBig extends Constraint("NotTooBig", (_ < 1000))
case object NonZero extends Constraint("NonZero", (_ != 0))
case class NotEquals(x: Int) extends Constraint("NotEquals " + x, (_ != x))

object Main {

  def eval(ctrs: Seq[Constraint])(x: Int): Boolean =
    (true /: ctrs){ case (accum, ctr) => accum && ctr.verifier(x) }

  def main(args: Array[String]) {
    val ctrs = NotTooBig :: NotEquals(5) :: Nil
    val evaluate = eval(ctrs) _

    println(evaluate(3000))
    println(evaluate(3))
    println(evaluate(5))
  }

}
Ron
fonte
Característica selada com objetos de caso também é uma possibilidade.
Ashalynd
2
O padrão "traço selado + objetos de caso" possui problemas detalhados em uma resposta do StackOverflow. No entanto, eu fiz descobrir como resolver todas as questões relacionadas com este padrão que também é coberto com a thread: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
7

acabou de descobrir o enumerado . é incrível e igualmente incrível, não é mais conhecido!

prática
fonte
2

Depois de fazer uma extensa pesquisa sobre todas as opções em torno de "enumerações" no Scala, publiquei uma visão geral muito mais completa desse domínio em outro thread do StackOverflow . Ele inclui uma solução para o padrão "traço selado + objeto de caso" em que resolvi o problema de pedido de inicialização de classe / objeto da JVM.

chaotic3quilibrium
fonte
1

Dotty (Scala 3) terá enumerações nativas suportadas. Confira aqui e aqui .

zeronona
fonte
1

Em Scala, é muito confortável com https://github.com/lloydmeta/enumeratum

Projeto é realmente bom com exemplos e documentação

Apenas este exemplo de seus documentos deve interessá-lo

import enumeratum._

sealed trait Greeting extends EnumEntry

object Greeting extends Enum[Greeting] {

  /*
   `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`

   You use it to implement the `val values` member
  */
  val values = findValues

  case object Hello   extends Greeting
  case object GoodBye extends Greeting
  case object Hi      extends Greeting
  case object Bye     extends Greeting

}

// Object Greeting has a `withName(name: String)` method
Greeting.withName("Hello")
// => res0: Greeting = Hello

Greeting.withName("Haro")
// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)

// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]
Greeting.withNameOption("Hello")
// => res1: Option[Greeting] = Some(Hello)

Greeting.withNameOption("Haro")
// => res2: Option[Greeting] = None

// It is also possible to use strings case insensitively
Greeting.withNameInsensitive("HeLLo")
// => res3: Greeting = Hello

Greeting.withNameInsensitiveOption("HeLLo")
// => res4: Option[Greeting] = Some(Hello)

// Uppercase-only strings may also be used
Greeting.withNameUppercaseOnly("HELLO")
// => res5: Greeting = Hello

Greeting.withNameUppercaseOnlyOption("HeLLo")
// => res6: Option[Greeting] = None

// Similarly, lowercase-only strings may also be used
Greeting.withNameLowercaseOnly("hello")
// => res7: Greeting = Hello

Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)
Dmitriy Kuzkin
fonte