Jak modelować typy wyliczeniowe bezpieczne dla typu?

311

Scala nie ma bezpiecznych typów, enumtakich jak Java. Biorąc pod uwagę zestaw powiązanych stałych, jaki byłby najlepszy sposób w Scali do przedstawienia tych stałych?

Jesper
źródło
2
Dlaczego po prostu nie używać enum Java? Jest to jedna z niewielu rzeczy, które nadal wolę używać zwykłego języka Java.
Max
1
Napisałem krótki przegląd wyliczeń Scala i alternatyw, które mogą ci się przydać: pedrorijo.com/blog/scala-enums/
pedrorijo91

Odpowiedzi:

187

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

Przykładowe użycie

  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
źródło
2
Poważnie, nie należy używać aplikacji. NIE zostało to naprawione; wprowadzono nową klasę, App, która nie ma problemów, o których wspomniał Schildmeijer. Podobnie jak „obiekt foo rozszerza aplikację {...}” I masz natychmiastowy dostęp do argumentów wiersza poleceń poprzez zmienną args.
AmigoNico,
scala.Enumeration (którego używasz w powyższym przykładzie kodu obiektu „WeekDay” powyżej) nie oferuje wyczerpującego dopasowania wzorca. Przebadałem wszystkie różne wzorce wyliczania obecnie stosowane w Scali i podałem je i omówiłem w tej odpowiedzi StackOverflow (w tym nowy wzorzec, który oferuje zarówno Scala.Enumeration, jak i wzorzec „sealed trait + case object”: stackoverflow. com / a / 25923651/501113
chaotic3quilibrium,
377

Muszę powiedzieć, że przykład skopiowany z powyższej dokumentacji Scali przez skaffmana ma ograniczoną użyteczność w praktyce (równie dobrze możesz użyć case objects).

Aby uzyskać coś najbardziej przypominającego Javę Enum(tj. Z rozsądnymi toStringi valueOfmetodami - być może utrwalasz wartości wyliczeniowe w bazie danych), musisz ją nieco zmodyfikować. Jeśli używałeś kodu skaffmana :

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

Podczas korzystania z następującej deklaracji:

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

Otrzymujesz bardziej sensowne wyniki:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
źródło
7
Btw. Metoda valueOf jest teraz martwa :-(
greenoldman
36
Zastąpienie @macias valueOfjest takie withName, które nie zwraca Opcji i rzuca NSE, jeśli nie ma dopasowania. Co!
Bluu
6
@Bluu Możesz dodać wartość siebie: def valueOf (name: String) = WeekDay.values.find (_. ToString == name), aby mieć opcję
centr
@centr Kiedy próbuję utworzyć a Map[Weekday.Weekday, Long]i dodać wartość powiedzieć Mondo niego, kompilator zgłasza błąd typu nieprawidłowego. Oczekiwany dzień tygodnia. Dzień tygodnia znalazł wartość? Dlaczego to się dzieje?
Sohaib,
@Sohaib Powinna to być mapa [Weekday.Value, Long].
środkowy
98

Istnieje wiele sposobów działania.

1) Użyj symboli. Nie zapewni ci jednak żadnego bezpieczeństwa, poza tym, że nie akceptujesz nie-symboli, w których spodziewany jest symbol. Wspominam o tym tutaj tylko dla kompletności. Oto przykład użycia:

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) Korzystanie z klasy Enumeration:

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

lub, jeśli chcesz go serializować lub wyświetlić:

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

Można to wykorzystać w następujący sposób:

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 /

Niestety nie gwarantuje, że wszystkie mecze zostaną uwzględnione. Gdybym zapomniał umieścić Row lub Column w meczu, kompilator Scala nie ostrzegłby mnie. Daje mi to pewne bezpieczeństwo, ale nie tyle, ile można uzyskać.

3) Obiekty sprawy:

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

Teraz, jeśli pominę sprawę match, kompilator ostrzeże mnie:

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

    what match {
    ^
one warning found

Jest używany prawie w ten sam sposób i nawet nie potrzebuje 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 /

Zastanawiasz się zatem, dlaczego kiedykolwiek używać Wyliczenia zamiast obiektów sprawy. W rzeczywistości obiekty przypadków mają wiele zalet, na przykład tutaj. Klasa Enumeration ma jednak wiele metod Collection, takich jak elementy (iterator na Scali 2.8), które zwracają Iterator, mapę, flatMap, filtr itp.

Ta odpowiedź jest zasadniczo wybranymi częściami tego artykułu na moim blogu.

Daniel C. Sobral
źródło
„... nie akceptując symboli innych niż oczekiwany symbol”> Zgaduję, że masz na myśli, że Symbolinstancje nie mogą zawierać spacji ani znaków specjalnych. Większość osób, które spotykają się z Symbolklasą po raz pierwszy, prawdopodobnie tak uważa, ale w rzeczywistości jest to nieprawidłowe. Symbol("foo !% bar -* baz")kompiluje się i działa idealnie dobrze. Innymi słowy, możesz idealnie tworzyć Symbolinstancje owijające dowolny ciąg znaków (po prostu nie możesz tego zrobić z cukrem składniowym „pojedyncza koma”). Jedyną rzeczą, Symbolktóra gwarantuje, jest wyjątkowość każdego danego symbolu, dzięki czemu porównanie i dopasowanie jest nieznacznie szybsze.
Régis Jean-Gilles
@ RégisJean-Gilles Nie, mam na myśli, że nie można przekazać Stringna przykład argumentu Symbolparametru.
Daniel C. Sobral,
Tak, zrozumiałem tę część, ale jest to dość sporna kwestia, jeśli zastąpisz Stringinną klasą, która jest w zasadzie owijką wokół łańcucha i może być swobodnie konwertowana w obu kierunkach (jak ma to miejsce Symbol). Myślę, że właśnie to miałeś na myśli mówiąc „Nie da to żadnego bezpieczeństwa”, po prostu nie było to bardzo jasne, biorąc pod uwagę, że OP wyraźnie poprosił o bezpieczne rozwiązania typu. Nie byłem pewien, czy w momencie pisania wiedziałeś, że nie tylko nie jest to bezpieczne, ponieważ w ogóle nie są to wyliczenia, ale także Symbol nie gwarantuje, że przekazany argument nie będzie zawierał specjalnych znaków.
Régis Jean-Gilles
1
Aby rozwinąć, mówiąc „nie akceptując nie-symbole tam, gdzie oczekuje się symbolu”, można to rozumieć jako „nieakceptowanie wartości, które nie są instancjami Symbolu” (co jest oczywiście prawdą) lub „nieakceptowanie wartości, które nie są zwykły identyfikator podobny strun, znany jako "symbole” (co nie jest prawdą, a to błędne przekonanie, że prawie każdy ma po raz pierwszy spotykamy symbole Scala, z uwagi na fakt, że pierwsze spotkanie jest jednak specjalnej 'foonotacji który dokłada wyklucza ciągi nie będące identyfikatorami). To nieporozumienie, które chciałem rozwiać dla każdego przyszłego czytelnika.
Régis Jean-Gilles
@ RégisJean-Gilles Miałem na myśli tę pierwszą, tę, która jest oczywiście prawdą. Mam na myśli, że jest to oczywiście prawda dla każdego, kto jest przyzwyczajony do pisania statycznego. Wtedy było wiele dyskusji na temat względnych zalet pisania statycznego i „dynamicznego”, a wiele osób zainteresowanych Scalą pochodziło z dynamicznego pisania, więc pomyślałem, że nie było to oczywiste. W dzisiejszych czasach nie pomyślałbym nawet o tej uwadze. Osobiście uważam, że Symbol Scali jest brzydki i zbędny i nigdy go nie używam. Głosuję za twoim ostatnim komentarzem, ponieważ jest to dobry punkt.
Daniel C. Sobral
52

Nieco bardziej szczegółowy sposób deklarowania nazwanych wyliczeń:

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

Oczywiście problem polega na tym, że trzeba będzie zsynchronizować kolejność nazw i valsów, co jest łatwiejsze, jeśli nazwa i val są zadeklarowane w tym samym wierszu.

Walter Chang
źródło
11
Na pierwszy rzut oka wygląda to na czystsze, ale ma tę wadę, że wymaga od opiekuna synchronizacji obu list. Na przykład dni tygodnia wydaje się mało prawdopodobne. Ale ogólnie nowa wartość może zostać wstawiona lub jedna usunięta, a dwie listy mogą nie być zsynchronizowane, w takim przypadku można wprowadzić subtelne błędy.
Brent Faust
1
Zgodnie z poprzednim komentarzem ryzyko polega na tym, że dwie różne listy mogą po cichu zsynchronizować się. Chociaż nie jest to problemem dla twojego obecnego małego przykładu, jeśli jest o wiele więcej członków (jak w dziesiątkach do setek), szanse na dwie listy cicho zsynchronizowane są znacznie wyższe. Również scala. Wyliczenia nie mogą korzystać z wyczerpującego czasu kompilacji Scala dopasowywania ostrzeżeń / błędów. Utworzyłem odpowiedź StackOverflow, która zawiera rozwiązanie sprawdzające środowisko wykonawcze w celu zapewnienia synchronizacji dwóch list: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Możesz użyć zapieczętowanej klasy abstrakcyjnej zamiast wyliczenia, na przykład:

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
źródło
Możliwa jest również zapieczętowana cecha z obiektami skrzynek.
Ashalynd
2
Wzorzec „uszczelniona cecha + obiekty przypadków” zawiera problemy, które szczegółowo opisuję w odpowiedzi StackOverflow. Jednak
wymyśliłem
7

właśnie odkryłem enumeratum . jest niesamowity i równie niesamowity, nie jest bardziej znany!

praktyczny
źródło
2

Po dogłębnych badaniach wszystkich opcji dotyczących „wyliczeń” w Scali, opublikowałem znacznie pełniejszy przegląd tej domeny w innym wątku StackOverflow . Zawiera rozwiązanie wzoru „uszczelniona cecha + obiekt sprawy”, w którym rozwiązałem problem z kolejnością inicjowania klasy / obiektu JVM.

chaotic3quilibrium
źródło
1

Dotty (Scala 3) będzie obsługiwał natywne wyliczenia. Sprawdź tutaj i tutaj .

zeronone
źródło
1

W Scali jest bardzo wygodnie z https://github.com/lloydmeta/enumeratum

Projekt jest naprawdę dobry z przykładami i dokumentacją

Właśnie ten przykład z ich dokumentów powinien cię zainteresować

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
źródło