Obiekty przypadków vs Wyliczenia w Scali


231

Czy istnieją jakieś wytyczne dotyczące najlepszych praktyk dotyczące tego, kiedy używać klas przypadków (lub obiektów spraw) zamiast rozszerzania Wyliczania w Scali?

Wydaje się, że oferują te same korzyści.


2
Napisałem krótki przegląd wyliczeń Scala i alternatyw, które mogą ci się przydać: pedrorijo.com/blog/scala-enums/
pedrorijo91

1
Zobacz także Scala 3 opartaenum na Dotty (z połowy 2020 r.).
VCC

Odpowiedzi:


223

Jedną dużą różnicą jest to, że Enumerationsą obsługiwane przez tworzenie ich z jakiegoś nameStringa. Na przykład:

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

Następnie możesz zrobić:

val ccy = Currency.withName("EUR")

Jest to przydatne, gdy chcesz zachować wyliczenia (na przykład w bazie danych) lub utworzyć je z danych znajdujących się w plikach. Jednak ogólnie uważam, że wyliczenia są nieco nieporadne w Scali i mam wrażenie niezręcznego dodatku, więc teraz mam tendencję do używania case objects. A case objectjest bardziej elastyczny niż wyliczenie:

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

case class UnknownCurrency(name: String) extends Currency

Więc teraz mam tę zaletę ...

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

As @ chaotic3quilibrium wskazał (z pewnymi poprawkami ułatwiającymi czytanie):

Jeśli chodzi o wzorzec „UnknownCurrency (code)”, istnieją inne sposoby radzenia sobie ze znalezieniem łańcucha kodu waluty niż „zerwanie” zamkniętego zestawu tego Currencytypu. UnknownCurrencybyć w typieCurrency może teraz zakraść się do innych części API.

Wskazane jest, aby wypchnąć tę skrzynkę na zewnątrz Enumerationi zmusić klienta do radzenia sobie z Option[Currency]typem, który wyraźnie wskazywałby, że naprawdę istnieje pasujący problem i „zachęcał” użytkownika interfejsu API do samodzielnego rozwiązania problemu.

Aby odpowiedzieć na inne odpowiedzi tutaj, główne wady case objects ponad Enumerations to:

  1. Nie można iterować we wszystkich przypadkach „wyliczenia” . Z pewnością tak jest, ale w praktyce stwierdziłem, że jest to niezwykle rzadkie.

  2. Nie można łatwo utworzyć instancji z utrwalonej wartości . Jest to również prawdą, ale z wyjątkiem dużych wyliczeń (na przykład wszystkich walut), nie stanowi to dużego obciążenia.


10
Inną różnicą jest to, że wyliczenie wyliczenia jest uporządkowane po wyjęciu z pudełka, podczas gdy wyliczenie oparte na przypadku nie jest oczywiste
om-nom-nom,

1
Inną kwestią dla obiektów sprawy jest to, czy zależy Ci na interoperacyjności Java. Wyliczenie zwróci wartości jako Enumeration.Value, a zatem 1) wymaga biblioteki scala, 2) utraty informacji o typie rzeczywistym.
juanmirocks

7
@oxbow_lakes Odnośnie do punktu 1, a konkretnie tej części „... W praktyce zdarza mi się, że jest to niezwykle rzadkie”: Najwyraźniej rzadko wykonujesz dużo pracy w interfejsie użytkownika. Jest to niezwykle powszechny przypadek użycia; wyświetlenie (rozwijanej) listy prawidłowych elementów wyliczenia, z których można wybrać.
chaotic3quilibrium,

Nie rozumiem, jaki typ przedmiotu jest dopasowany trade.ccyw przykładzie zapieczętowanej cechy.
lloth

i czy nie case objectgenerują większego (~ 4x) śladu kodu niż Enumeration? Przydatne rozróżnienie, szczególnie w przypadku scala.jsprojektów wymagających małej powierzchni.
ecoe

69

AKTUALIZACJA: Utworzono nowe rozwiązanie oparte na makrach , które jest znacznie lepsze niż rozwiązanie, które przedstawiam poniżej. Zdecydowanie polecam korzystanie z tego nowego rozwiązania opartego na makrach . I wydaje się, że plany Dotty sprawią, że ten styl rozwiązania enum będzie częścią języka. Whoohoo!

Podsumowanie:
Istnieją trzy podstawowe wzorce próby odtworzenia Javy Enumw ramach projektu Scala. Dwa z trzech wzorów; bezpośrednio przy użyciu Java Enumi scala.Enumerationnie są w stanie umożliwić wyczerpującego dopasowania wzorca Scali. I trzeci; „Sealed trait + case object”, ma… ale ma komplikacje związane z inicjalizacją klasy / obiektu JVM, co powoduje niespójne generowanie indeksu porządkowego.

Stworzyłem rozwiązanie z dwiema klasami; Wyliczenie i wyliczenie Udekorowane , znajdujące się w tej Gist . Nie opublikowałem kodu w tym wątku, ponieważ plik wyliczenia był dość duży (+400 wierszy - zawiera wiele komentarzy wyjaśniających kontekst implementacji).

Szczegóły:
Pytanie, które zadajesz, jest dość ogólne; „... kiedy używać caseklasobjects zamiast rozszerzania [scala.]Enumeration”. I okazuje się, że istnieje WIELE możliwych odpowiedzi, każda odpowiedź zależy od subtelności określonych wymagań projektu. Odpowiedź można zredukować do trzech podstawowych wzorów.

Na początek upewnijmy się, że pracujemy od tego samego podstawowego pojęcia, czym jest wyliczenie. Zdefiniujmy wyliczenie głównie w kategoriach Enumdostarczonych od Java 5 (1.5) :

  1. Zawiera naturalnie uporządkowany zamknięty zestaw nazwanych członków
    1. Istnieje stała liczba członków
    2. Członkowie są naturalnie uporządkowani i jawnie indeksowani
      • W przeciwieństwie do sortowania na podstawie niektórych kryteriów kwerendy członków inate
    3. Każdy członek ma unikalną nazwę w całym zestawie wszystkich członków
  2. Wszystkich członków można łatwo iterować na podstawie ich indeksów
  3. Członka można odzyskać za pomocą jego nazwy (z rozróżnianiem wielkości liter)
    1. Byłoby całkiem miło, gdyby można było odzyskać członka o nazwie bez rozróżniania wielkości liter
  4. Członka można odzyskać za pomocą jego indeksu
  5. Członkowie mogą łatwo, przejrzyście i skutecznie korzystać z serializacji
  6. Członków można łatwo rozszerzyć, aby przechowali dodatkowe powiązane dane dotyczące singletonowości
  7. Myśląc poza Javą Enum, byłoby miło móc jawnie wykorzystać sprawdzanie wyczerpania dopasowania wzorca Scali w celu wyliczenia

Następnie spójrzmy na sprowadzone wersje trzech najczęściej publikowanych wzorców rozwiązań:

A) Właściwie bezpośrednio przy użyciu wzorca JavaEnum (w mieszanym projekcie 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;
  }
}

Następujące elementy z definicji wyliczenia nie są dostępne:

  1. 3.1 - Byłoby całkiem miło, gdyby można było odzyskać członka o nazwie bez rozróżniania wielkości liter
  2. 7 - Myśląc poza enumem Javy, byłoby miło móc jawnie wykorzystać sprawdzanie wyczerpania w dopasowaniu wzorca Scali w celu wyliczenia

W przypadku moich bieżących projektów nie czerpię korzyści z ryzyka związanego ze ścieżką projektów mieszanych Scala / Java. I nawet gdybym mógł wybrać projekt mieszany, punkt 7 jest krytyczny, ponieważ pozwala mi wychwycić problemy z czasem kompilacji, jeśli / kiedy dodam / usunę elementy wyliczeniowe lub piszę nowy kod, aby poradzić sobie z istniejącymi elementami wyliczającymi.


B) Używając wzorca „ 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}
}

Następujące elementy z definicji wyliczenia nie są dostępne:

  1. 1.2 - Członkowie są naturalnie uporządkowani i jawnie indeksowani
  2. 2 - Wszystkich członków można łatwo iterować na podstawie ich indeksów
  3. 3 - Członka można odzyskać za pomocą jego nazwy (z rozróżnieniem małych i wielkich liter)
  4. 3.1 - Byłoby całkiem miło, gdyby można było odzyskać członka o nazwie bez rozróżniania wielkości liter
  5. 4 - Członka można odzyskać za pomocą jego indeksu

Można argumentować, że naprawdę spełnia pozycje 5 i 6. definicji wyliczenia. W przypadku 5 trudno jest stwierdzić, że jest wydajna. W przypadku wersji 6 rozszerzenie nie jest tak naprawdę łatwe do przechowywania dodatkowych powiązanych danych dotyczących singletonowości.


C) Używając scala.Enumerationwzorca (zainspirowanego odpowiedzią 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]
}

Następujące elementy z definicji wyliczenia nie są dostępne (okazuje się, że są identyczne z listą do bezpośredniego używania Java Enum):

  1. 3.1 - Byłoby całkiem miło, gdyby można było odzyskać członka o nazwie bez rozróżniania wielkości liter
  2. 7 - Myśląc poza enumem Javy, byłoby miło móc jawnie wykorzystać sprawdzanie wyczerpania w dopasowaniu wzorca Scali w celu wyliczenia

Ponownie w moich bieżących projektach punkt 7 ma kluczowe znaczenie dla umożliwienia mi wychwycenia problemów z czasem kompilacji, jeśli / kiedy dodam / usunę elementy wyliczeniowe lub piszę nowy kod, aby poradzić sobie z istniejącymi elementami wyliczającymi.


Biorąc pod uwagę powyższą definicję wyliczenia, żadne z powyższych trzech rozwiązań nie działa, ponieważ nie zapewniają one wszystkiego, co opisano w powyższej definicji wyliczenia:

  1. Java Enum bezpośrednio w mieszanym projekcie Scala / Java
  2. „cecha zamknięta + obiekty skrzynek”
  3. scala.Enumeration

Każde z tych rozwiązań można ostatecznie przerobić / rozszerzyć / zrefaktoryzować, aby spróbować zaspokoić niektóre z brakujących wymagań każdego z nich. Jednak ani Java, Enumaniscala.Enumeration rozwiązania nie mogą być wystarczająco rozbudowane, aby zapewnić pozycję 7. A dla moich własnych projektów jest to jedna z bardziej przekonujących wartości używania zamkniętego typu w Scali. Zdecydowanie wolę kompilować ostrzeżenia / błędy czasowe, aby wskazać, że mam lukę / problem w moim kodzie, zamiast konieczności zbierania go z wyjątku / awarii środowiska wykonawczego.


W tym względzie zacząłem pracować ze case objectścieżką, aby sprawdzić, czy mogę stworzyć rozwiązanie, które obejmowałoby wszystkie powyższe definicje wyliczeń. Pierwszym wyzwaniem było przepchnięcie rdzenia problemu inicjowania klasy / obiektu JVM (szczegółowo opisanego w tym poście StackOverflow ). I w końcu udało mi się znaleźć rozwiązanie.

Ponieważ moim rozwiązaniem są dwie cechy; Wyliczanie i wyliczanie Udekorowane , a ponieważ Enumerationcecha ma ponad +400 linii (wiele komentarzy wyjaśniających kontekst), rezygnuję z wklejania jej do tego wątku (co spowodowałoby znaczne rozciągnięcie strony). Aby uzyskać szczegółowe informacje, przejdź bezpośrednio do Gist .

Oto, jak rozwiązanie wygląda tak, używając tego samego pomysłu danych jak powyżej (w pełni skomentowana wersja dostępna tutaj ) i wdrożonego w 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
}

Jest to przykładowe użycie nowej pary cech wyliczenia, które utworzyłem (znajdujących się w tej Gist ) w celu wdrożenia wszystkich możliwości pożądanych i opisanych w definicji wyliczenia.

Jednym z wyrażonych problemów jest to, że nazwy członków wyliczenia muszą zostać powtórzone ( decorationOrderedSetw powyższym przykładzie). Chociaż zminimalizowałem to do jednego powtórzenia, nie mogłem zobaczyć, jak to zrobić jeszcze mniej z powodu dwóch problemów:

  1. Inicjalizacja obiektu / klasy JVM dla tego konkretnego obiektu / modelu obiektu sprawy jest niezdefiniowana (zobacz ten wątek Stackoverflow )
  2. Treść zwracana z metody getClass.getDeclaredClassesma niezdefiniowaną kolejność (i jest mało prawdopodobne, aby była w tej samej kolejności, co case objectdeklaracje w kodzie źródłowym)

Biorąc pod uwagę te dwa problemy, musiałem zrezygnować z próby wygenerowania dorozumianego zamówienia i musiałem wyraźnie wymagać od klienta zdefiniowania i zadeklarowania go za pomocą jakiegoś uporządkowanego pojęcia zestawu. Ponieważ kolekcje Scali nie mają implementacji zestawu z wstawionymi wstawkami, najlepsze, co mogłem zrobić, to Listsprawdzić, a następnie sprawdzić, czy był to naprawdę zestaw. Nie tak wolałbym to osiągnąć.

Biorąc pod uwagę, że projekt wymagał drugiego uporządkowania listy / zestawu val, biorąc pod uwagę ChessPiecesEnhancedDecoratedpowyższy przykład, można było dodać, case object PAWN2 extends Membera następnie zapomnieć o dodaniu Decoration(PAWN2,'P2', 2)do decorationOrderedSet. Tak więc sprawdzanie środowiska wykonawczego sprawdza, czy lista nie jest tylko zestawem, ale zawiera WSZYSTKIE obiekty sprawy, które rozszerzają sealed trait Member. To była specjalna forma refleksji / makro piekła do przepracowania.


Zostaw komentarz i / lub opinię na temat Gist .


Mam teraz ukazała się pierwsza wersja biblioteki ScalaOlio (GPLv3), który zawiera bardziej up-to-date wersje zarówno org.scalaolio.util.Enumerationa org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium

I przeskoczyć bezpośrednio do repozytorium ScalaOlio na Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium

5
Jest to wysokiej jakości odpowiedź i wiele z niej należy wziąć. Dziękuję
angabriel

1
Wygląda na to, że Odersky chce ulepszyć Dotty (przyszłą Scalę 3.0) z rodzimym wyliczeniem. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium

62

Obiekty Case już zwracają swoją nazwę dla swoich metod toString, więc przekazywanie jej osobno nie jest konieczne. Oto wersja podobna do jho (metody wygody pominięte ze względu na zwięzłość):

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

Obiekty są leniwe; za pomocą vals zamiast tego możemy usunąć listę, ale musimy powtórzyć nazwę:

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

Jeśli nie masz nic przeciwko oszukiwaniu, możesz wstępnie załadować wartości wyliczenia za pomocą interfejsu API odbicia lub czegoś takiego jak Google Reflections. Nie leniwe obiekty wielkości liter zapewniają najczystszą składnię:

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
}

Ładne i czyste, ze wszystkimi zaletami klas spraw i wyliczeń Java. Osobiście definiuję wartości wyliczania poza obiektem, aby lepiej dopasować idiomatyczny kod Scala:

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

3
jedno pytanie: ostatnie rozwiązanie nazywa się „obiektami leniwymi”, ale w tym przypadku obiekty nie są ładowane, dopóki ich nie użyjemy: dlaczego nazywacie to rozwiązanie leniwym?
Seb Cesbron

2
@Noel, musisz użyć: wklej, aby wkleić całą zapieczętowaną hierarchię do REPL. Jeśli tego nie zrobisz, pojedyncza linia z zapieczętowaną klasą podstawową / cechą liczy się jako pojedynczy plik, zostaje natychmiast zapieczętowana i nie może zostać przedłużona w następnej linii.
Jürgen Strobel

2
@GatesDA Tylko pierwszy fragment kodu nie zawiera błędu (ponieważ wyraźnie wymagasz od klienta deklarowania i definiowania wartości. Zarówno drugie, jak i trzecie rozwiązanie ma subtelny błąd, który opisałem w moim ostatnim komentarzu (jeśli klient ma dostęp do waluty .GBP bezpośrednio i po pierwsze, lista wartości będzie „zepsuta”). Zagłębiłem się dokładnie w domenę wyliczania Scali i szczegółowo opisałem ją w mojej odpowiedzi na ten sam wątek: stackoverflow.com/a/25923651/501113
chaotic3quilibrium,

1
Być może jedną z wad tego podejścia (w przeciwieństwie do Java Enums) jest to, że po wpisaniu Waluty <dot> w IDE nie wyświetla dostępnych opcji.
Ivan Balashov

1
Jak wspomniano w @SebCesbron, obiekty przypadków są tutaj leniwe. Więc jeśli zadzwonię Currency.values, odzyskam tylko te wartości, do których wcześniej uzyskałem dostęp. Czy jest na to jakiś sposób?
Sasgorilla,

27

Zalety korzystania z klas spraw w porównaniu z wyliczeniami to:

  • Korzystając z zapieczętowanych klas spraw, kompilator Scala może stwierdzić, czy dopasowanie jest w pełni określone, np. Kiedy wszystkie możliwe dopasowania są zawarte w deklaracji dopasowania. W przypadku wyliczeń kompilator Scala nie może stwierdzić.
  • Klasy spraw naturalnie obsługują więcej pól niż wyliczenie oparte na wartości, które obsługuje nazwę i identyfikator.

Korzyści z używania Wyliczeń zamiast klas spraw są:

  • Wyliczenia zazwyczaj wymagają nieco mniej kodu do napisania.
  • Wyliczenia są nieco łatwiejsze do zrozumienia dla kogoś nowego w Scali, ponieważ są one powszechne w innych językach

Ogólnie rzecz biorąc, jeśli potrzebujesz tylko listy prostych stałych według nazwy, użyj wyliczeń. W przeciwnym razie, jeśli potrzebujesz czegoś bardziej złożonego lub chcesz dodatkowego bezpieczeństwa kompilatora informującego, czy masz określone wszystkie dopasowania, użyj klas przypadków.


15

AKTUALIZACJA: Poniższy kod zawiera błąd opisany tutaj . Poniższy program testowy działa, ale jeśli użyjesz DayOfWeek.Mon (na przykład) przed samym DayOfWeek, zakończy się niepowodzeniem, ponieważ DayOfWeek nie został zainicjowany (użycie obiektu wewnętrznego nie powoduje zainicjowania obiektu zewnętrznego). Nadal możesz używać tego kodu, jeśli robisz coś val enums = Seq( DayOfWeek )w swojej klasie głównej, zmuszając do inicjacji swoich wyliczeń lub możesz użyć modyfikacji chaotycznego 3quilibrium. Czekamy na wyliczenie oparte na makrach!


Jeśli chcesz

  • ostrzeżenia o niewyczerpujących wzorach dopasowania
  • Int ID przypisany do każdej wartości wyliczenia, którą można opcjonalnie kontrolować
  • niezmienna lista wartości wyliczeniowych, w kolejności, w jakiej zostały zdefiniowane
  • niezmienna mapa od nazwy do wartości wyliczeniowej
  • niezmienna mapa od id do wartości wyliczeniowej
  • miejsca do przyklejenia metod / danych dla wszystkich lub poszczególnych wartości wyliczenia lub dla wyliczenia jako całości
  • uporządkowane wartości wyliczeniowe (abyś mógł na przykład sprawdzić, czy dzień <środa)
  • możliwość rozszerzenia jednego wyliczenia, aby stworzyć inne

wówczas mogą być interesujące następujące rzeczy. Witamy mile widziane.

W tej implementacji istnieją abstrakcyjne klasy podstawowe Enum i EnumVal, które rozszerzamy. Za chwilę zobaczymy te klasy, ale najpierw oto, jak zdefiniujesz wyliczenie:

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

Zauważ, że musisz użyć każdej wartości wyliczenia (wywołaj jej metodę zastosuj), aby wprowadzić ją w życie. [Chciałbym, żeby przedmioty wewnętrzne nie były leniwe, chyba że o to poproszę. Myślę.]

Możemy oczywiście dodawać metody / dane do obiektów DayOfWeek, Val lub indywidualnych obiektów przypadków, jeśli chcemy.

A oto jak użyłbyś takiego wyliczenia:

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

}

Oto, co otrzymujesz po skompilowaniu:

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

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

Możesz zamienić „dopasowanie dnia” na „dopasowanie dnia (dzień: @ odznaczone)”, jeśli nie chcesz takich ostrzeżeń, lub po prostu dołącz na końcu sprawę typu „catch-all”.

Po uruchomieniu powyższego programu otrzymujesz następujące dane wyjściowe:

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

Zauważ, że ponieważ Lista i Mapy są niezmienne, możesz łatwo usuwać elementy, aby tworzyć podzbiory, bez przerywania samego wyliczenia.

Oto sama klasa Enum (i EnumVal w niej):

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

}

A oto jego bardziej zaawansowane zastosowanie, które kontroluje identyfikatory i dodaje dane / metody do abstrakcji Val i samego wyliczenia:

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

Tyvm za zapewnienie tego. Bardzo to doceniam. Zauważam jednak, że używa „var” w przeciwieństwie do val. I to jest graniczny grzech śmiertelny w świecie FP. Czy istnieje sposób na wdrożenie tego w taki sposób, że nie ma zastosowania var? Ciekawe, czy jest to przypadek krawędziowy typu FP i nie rozumiem, w jaki sposób implementacja jest niepożądana w FP.
chaotic3quilibrium,

2
Prawdopodobnie nie mogę ci pomóc. Scala dość często pisze klasy, które mutują wewnętrznie, ale są niezmienne dla tych, którzy ich używają. W powyższym przykładzie użytkownik DayOfWeek nie może mutować wyliczenia; nie można na przykład zmienić identyfikatora wtorku ani jego nazwy po fakcie. Ale jeśli chcesz implementacji, która jest wewnętrznie wolna od mutacji , to nie mam nic. Nie zdziwiłbym się jednak, widząc fajną nową funkcję wyliczania opartą na makrach w 2.11; pomysły są wyrzucane na scala-lang.
AmigoNico,

Otrzymuję dziwny błąd w arkuszu Scala. Jeśli bezpośrednio używam jednej z instancji Value, pojawia się błąd inicjalizacji. Jeśli jednak wywołam metodę .values, aby zobaczyć zawartość wyliczenia, to zadziała, a następnie zadziała bezpośrednio przy użyciu instancji wartości. Masz pojęcie, jaki jest błąd inicjalizacji? A jaki jest optymalny sposób zapewnienia, że ​​inicjalizacja nastąpi we właściwej kolejności, niezależnie od konwencji wywoływania?
chaotic3quilibrium

@ chaotic3quilibrium: Wow! Dziękujemy za kontynuowanie tego i oczywiście dzięki Rexowi Kerrowi za podnoszenie ciężarów. Wspomnę tutaj o problemie i odniosę się do utworzonego pytania.
AmigoNico,

„[Używanie var] jest granicznym grzechem śmiertelnym w świecie FP” - nie sądzę, że opinia ta jest powszechnie akceptowana.
Erik Kaplun,

12

Mam tutaj fajną prostą bibliotekę lib, która pozwala ci używać zapieczętowanych cech / klas jako wartości wyliczeniowych bez konieczności utrzymywania własnej listy wartości. Opiera się na prostym makrze, które nie jest zależne od buggy knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum


10

Aktualizacja z marca 2017 r .: jak skomentował Anthony Accioly , scala.Enumeration/enumPR został zamknięty.

Dotty (kompilator nowej generacji dla Scali) przejmie wiodącą rolę, choć wydanie dotty 1970 i PR Martina Oderskiego 1958 .


Uwaga: istnieje teraz (sierpień 2016, ponad 6 lat później) propozycja usunięcia scala.Enumeration: PR 5352

Przestarzałe scala.Enumeration, dodaj @enumadnotację

Składnia

@enum
 class Toggle {
  ON
  OFF
 }

jest możliwym przykładem implementacji, intencją jest także wsparcie ADT, które są zgodne z pewnymi ograniczeniami (brak zagnieżdżania, rekurencji lub zmieniania parametrów konstruktora), np .:

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

Przestarzała, która jest katastrofą nieoznaczoną scala.Enumeration.

Zalety @enum w porównaniu do scala.

  • Właściwie działa
  • Interop Java
  • Brak problemów z usuwaniem
  • Nie ma mylącego mini-DSL do nauki podczas definiowania wyliczeń

Wady: brak.

Rozwiązuje to problem braku możliwości posiadania jednej bazy kodu obsługującej Scala-JVM Scala.jsi Scala-Native (kod źródłowy Java nie jest obsługiwany Scala.js/Scala-Native, kod źródłowy Scala nie jest w stanie zdefiniować wyliczeń, które są akceptowane przez istniejące interfejsy API w Scala-JVM).


Powyższy PR został zamknięty (bez radości). Jest teraz 2017 i wygląda na to, że Dotty w końcu dostanie konstrukcję enum. Oto problem i PR Martina . Scal, scal, scal!
Anthony Accioly

8

Kolejna wada klas spraw w porównaniu z wyliczeniami, gdy trzeba będzie iterować lub filtrować we wszystkich instancjach. Jest to wbudowana funkcja wyliczania (i także wyliczenia Java), podczas gdy klasy przypadków nie obsługują automatycznie takiej możliwości.

Innymi słowy: „nie ma łatwego sposobu na uzyskanie listy całkowitego zestawu wyliczonych wartości z klasami przypadków”.


5

Jeśli poważnie myślisz o utrzymaniu współdziałania z innymi językami JVM (np. Java), najlepszą opcją jest pisanie wyliczeń Java. Działają one transparentnie zarówno z kodu Scala, jak i kodu Java, co jest więcej niż można powiedzieć o scala.Enumerationobiektach lub przypadkach. Nie będziemy mieć nowej biblioteki wyliczeń dla każdego nowego projektu hobby na GitHub, jeśli można tego uniknąć!


4

Widziałem różne wersje tworzenia klas przypadków naśladujących wyliczenie. Oto moja wersja:

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

Co pozwala budować klasy spraw, które wyglądają następująco:

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

Może ktoś mógłby wymyślić lepszy trik niż po prostu dodać każdą klasę spraw do listy, tak jak ja. To było wszystko, co mogłem wtedy wymyślić.


Dlaczego dwie osobne, niestosowne metody?
Saish

@ jho Próbowałem pracować nad obecnym rozwiązaniem, ale nie można go skompilować. W drugim fragmencie kodu znajduje się odniesienie do witryny w „type V = Site”. Nie jestem pewien, co to oznacza, aby usunąć błąd kompilacji. Następnie, dlaczego podajecie puste nawiasy klamrowe dla „waluty abstrakcyjnej klasy”? Czy nie można ich po prostu pominąć? Wreszcie, dlaczego używasz var w „var values ​​= ...”? Czy to nie oznacza, że ​​klienci mogą w dowolnym momencie z dowolnego miejsca w kodzie przypisać nową Listę do wartości? Czy nie byłoby o wiele lepiej, aby uczynić go val zamiast var?
chaotic3quilibrium,

2

Kilka razy zastanawiałem się nad tymi dwiema opcjami, kiedy kilka razy ich potrzebowałem. Do niedawna preferowałem opcję zapieczętowanej cechy obiektu / sprawy.

1) Deklaracja wyliczenia Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Zapieczętowane cechy + przedmioty skrzynek

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Chociaż żadne z nich tak naprawdę nie spełnia wszystkich warunków wyliczenia java, poniżej przedstawiono zalety i wady:

Wyliczenie Scala

Plusy: -Funkcje do tworzenia instancji z opcją lub bezpośredniego zakładania dokładności (łatwiejsze przy ładowaniu z trwałego sklepu) -Interacja wszystkich możliwych wartości jest obsługiwana

Wady: -Ostrzeżenie kompilacji dla niewyczerpującego wyszukiwania nie jest obsługiwane (sprawia, że ​​dopasowanie wzorca jest mniej idealne)

Obiekty przedmiotów / Zapieczętowane cechy

Plusy: -Używając cech zapieczętowanych, możemy wstępnie utworzyć instancję niektórych wartości, podczas gdy inne można wstrzykiwać podczas tworzenia - pełna obsługa dopasowania wzorców (zdefiniowane metody zastosowania / zastosowania)

Minusy: -Instantingowanie z trwałego sklepu - często musisz tutaj zastosować dopasowanie wzorca lub zdefiniować własną listę wszystkich możliwych „wartości wyliczeniowych”

To, co ostatecznie skłoniło mnie do zmiany zdania, to coś w rodzaju następującego fragmentu kodu:

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
}

Te .getrozmowy były ohydne - stosując wyliczenie zamiast mogę po prostu wywołać metodę withName na wyliczenie w sposób następujący:

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

Więc myślę, że preferuję używanie Wyliczeń, gdy dostęp do wartości ma być uzyskiwany z repozytorium, a w przeciwnym wypadku obiekty / cechy zapieczętowane.


Widzę, jak pożądany jest drugi wzorzec kodu (pozbywanie się dwóch metod pomocniczych z pierwszego wzorca kodu). Jednak wymyśliłem sposób, abyś nie musiał wybierać między tymi dwoma wzorami.
Obejmuję

2

wolę case objects (to kwestia osobistych preferencji). Aby poradzić sobie z problemami związanymi z tym podejściem (analizowanie łańcucha i iteracja po wszystkich elementach), dodałem kilka wierszy, które nie są idealne, ale są skuteczne.

Wklejam tutaj kod, oczekując, że może być przydatny, a także, że inni mogą go ulepszyć.

/**
 * 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 }
}

0

Dla tych, którzy wciąż szukają sposobu, aby uzyskać odpowiedź GatesDa na działanie : Możesz po prostu odwołać się do obiektu sprawy po zadeklarowaniu go do utworzenia instancji:

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
}

0

Myślę, że największą zaletą case classesnad enumerationstym jest to, że można użyć wzorca klasy typu aka polimorfizm ad-hoc . Nie musisz dopasowywać wyliczeń, takich jak:

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

zamiast tego będziesz mieć coś takiego:

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() = ...
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.