Случайные объекты и перечисления в Scala

231

Существуют ли рекомендации о том, когда использовать case-классы (или case-объекты) и расширять Enumeration в Scala?

Кажется, они предлагают некоторые из тех же преимуществ.

Алекс Миллер
источник
2
Я написал небольшой обзор о перечислении scala и его альтернативах, вы можете найти его полезным: pedrorijo.com/blog/scala-enums/
pedrorijo91
1
См. Также Scala 3enum на базе Dotty (для середины 2020 года).
VonC

Ответы:

223

Одно большое отличие состоит в том, что Enumeration они поддерживаются для создания экземпляров из некоторой nameстроки. Например:

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

Тогда вы можете сделать:

val ccy = Currency.withName("EUR")

Это полезно при желании сохранить перечисления (например, в базе данных) или создать их из данных, находящихся в файлах. Тем не менее, я нахожу в целом, что перечисления немного неуклюжи в Scala и имеют ощущение неуклюжего дополнения, поэтому я теперь склоняюсь к использованию case objects. case object более гибок, чем перечисление:

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

case class UnknownCurrency(name: String) extends Currency

Так что теперь у меня есть преимущество ...

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

Так как указал @ chaotic3quilibrium (с некоторыми исправлениями, облегчающими чтение):

Что касается шаблона «UnknownCurrency (code)», существуют другие способы обработки, не находящие строку кода валюты, кроме «нарушения» природы закрытого набора Currencyтипа.UnknownCurrencyтип Currencyтеперь может проникнуть в другие части API.

Желательно выдвинуть это дело наружу Enumeration и заставить клиента работать с Option[Currency]типом, который бы четко указывал на наличие действительно соответствующей проблемы и «поощрял» пользователя API разобраться с этим сам.

Чтобы уточнить другие ответы здесь, основные недостатки case objectS болееEnumeration s:

  1. Невозможно перебрать все экземпляры перечисления . Это, конечно, так, но на практике я обнаружил, что это крайне редко требуется.

  2. Не могу легко создать экземпляр из сохраняемой ценности . Это также верно, но, за исключением случая огромных перечислений (например, всех валют), это не представляет огромных накладных расходов.

oxbow_lakes
источник
10
Другое отличие состоит в том, что перечисление Enumeration упорядочено «из коробки», тогда как перечисление на основе падежа объекта явно не
om-nom-nom
1
Еще один момент для объектов case - это если вы заботитесь о совместимости Java. Перечисление будет возвращать значения как Enumeration.Value, таким образом 1) требуется scala-библиотека, 2) потеря фактической информации о типе.
juanmirocks
7
@oxbow_lakes Что касается пункта 1, в частности, этой части «... на практике я обнаружил, что это крайне редко требуется»: очевидно, вы редко выполняете много работы с пользовательским интерфейсом. Это чрезвычайно распространенный вариант использования; отображение (раскрывающегося) списка допустимых членов перечисления, из которых можно выбирать.
chaotic3quilibrium
Я не понимаю, какой тип предмета соответствует trade.ccyобразцу запечатанной черты.
Рот
и не case objectгенерирует ли (~ 4x) больше места для кода, чем Enumeration? Полезное различие, особенно для scala.jsпроектов, нуждающихся в небольшой площади.
ECOE
69

ОБНОВЛЕНИЕ: было создано новое решение на основе макросов, которое намного превосходит решение, которое я обрисовал в общих чертах ниже. Я настоятельно рекомендую использовать это новое решение на основе макросов . И, похоже, планы Дотти сделают этот стиль решения enum частью языка. Whoohoo!

Описание:
Есть три основных шаблона для попытки воспроизвести Java Enumв проекте Scala. Два из трех шаблонов; непосредственно с использованием Java Enumи scala.Enumerationне способны обеспечить полное сопоставление с образцом в Scala. И третий; «запечатанный trait + case object», делает ..., но имеет сложности инициализации класса / объекта JVM, приводящие к непоследовательному порядковому индексу генерации.

Я создал решение с двумя классами; Перечисление и Перечень оформлены , находятся в этом Гисте . Я не размещал код в этой теме, так как файл для Enumeration был довольно большим (+400 строк - содержит много комментариев, объясняющих контекст реализации).

Детали:
вопрос, который вы задаете, довольно общий; "... когда использовать caseклассыobjects против расширения[scala.]Enumeration ". И оказывается, что есть МНОГИЕ возможные ответы, каждый ответ зависит от тонкостей конкретных требований проекта, которые у вас есть. Ответ может быть уменьшен до трех основных моделей.

Для начала давайте удостоверимся, что мы работаем с той же базовой идеей, что и перечисление. Давайте определим перечисление в основном в терминах, Enumпредставленных в Java 5 (1.5) :

  1. Он содержит естественно упорядоченный закрытый набор именованных членов
    1. Есть фиксированное количество членов
    2. Члены естественно упорядочены и явно проиндексированы
      • В отличие от сортировки на основе некоторых критериев inate член
    3. Каждый участник имеет уникальное имя в общем наборе всех участников
  2. Все члены могут быть легко перебраны на основе их индексов
  3. Член может быть получен с его (чувствительным к регистру) именем
    1. Было бы неплохо, если бы член также мог быть получен с нечувствительным к регистру именем
  4. Член может быть восстановлен с его индексом
  5. Члены могут легко, прозрачно и эффективно использовать сериализацию
  6. Члены могут быть легко расширены для хранения дополнительных связанных одноэлементных данных
  7. Думая не только о Java Enum, было бы неплохо иметь возможность явно использовать проверку исчерпывающего соответствия шаблонов Scala для перечисления

Далее давайте рассмотрим выложенные версии трех самых распространенных шаблонов решений:

A) На самом деле напрямую с использованием шаблона JavaEnum (в смешанном проекте 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;
  }
}

Следующие элементы из определения перечисления недоступны:

  1. 3.1 - Было бы неплохо, если бы член также мог быть извлечен с нечувствительным к регистру именем
  2. 7. Думая не только о Enum в Java, было бы неплохо иметь возможность явно использовать проверку исчерпывающего соответствия шаблонов в Scala для перечисления

Для моих текущих проектов у меня нет преимущества рисковать по пути смешанного проекта Scala / Java. И даже если бы я мог выбрать смешанный проект, пункт 7 крайне важен для того, чтобы позволить мне улавливать проблемы времени компиляции, если / когда я либо добавляю / удаляю членов перечисления, либо пишу какой-то новый код для работы с существующими членами перечисления.


Б) Используя шаблон « 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}
}

Следующие элементы из определения перечисления недоступны:

  1. 1.2 - Члены естественно упорядочены и явно проиндексированы
  2. 2 - Все члены могут быть легко перебраны на основе их индексов
  3. 3 - член может быть найден с его (чувствительным к регистру) именем
  4. 3.1 - Было бы неплохо, если бы член также мог быть извлечен с нечувствительным к регистру именем
  5. 4 - член может быть найден с его индексом

Можно утверждать, что он действительно соответствует пунктам 5 и 6 определения перечисления. Для 5 сложно утверждать, что он эффективен. Для 6 не так-то просто расширять для хранения дополнительных связанных данных одиночности.


C) Используя scala.Enumerationобразец (вдохновленный этим ответом 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]
}

Следующие элементы из определения перечисления недоступны (оказывается, идентичны списку для непосредственного использования Java Enum):

  1. 3.1 - Было бы неплохо, если бы член также мог быть извлечен с нечувствительным к регистру именем
  2. 7. Думая не только о Enum в Java, было бы неплохо иметь возможность явно использовать проверку исчерпывающего соответствия шаблонов в Scala для перечисления

Опять же, для моих текущих проектов, пункт 7 имеет решающее значение для того, чтобы позволить мне улавливать проблемы времени компиляции, если / когда я либо добавляю / удаляю членов перечисления, либо пишу какой-то новый код для работы с существующими членами перечисления.


Таким образом, учитывая приведенное выше определение перечисления, ни одно из трех приведенных выше решений не работает, поскольку они не обеспечивают всего, что изложено в приведенном выше определении перечисления:

  1. Java Enum непосредственно в смешанном проекте Scala / Java
  2. «Запечатанная черта + тематические объекты»
  3. scala.Enumeration

Каждое из этих решений может быть в конечном счете переработано / расширено / реорганизовано, чтобы попытаться покрыть некоторые из недостающих требований каждого из них. Тем не менее, ни Java, Enumни scala.Enumerationрешения не могут быть достаточно расширены, чтобы обеспечить пункт 7. И для моих собственных проектов это одна из наиболее убедительных ценностей использования закрытого типа в Scala. Я настоятельно предпочитаю, чтобы предупреждения / ошибки во время компиляции указывали на то, что в моем коде есть пробел / проблема, а не необходимость выявлять его из исключения / сбоя производственной среды выполнения.


В связи с этим я приступил к работе с case objectпутем, чтобы посмотреть, смогу ли я найти решение, охватывающее все перечисленные выше определения. Первая задача состояла в том, чтобы протолкнуть ядро ​​проблемы инициализации класса / объекта JVM (подробно описано в этом посте StackOverflow ). И я наконец смог найти решение.

Поскольку мое решение - две черты; Enumeration и EnumerationDecorated , и поскольку эта Enumerationчерта имеет длину более + 400 строк (множество комментариев, объясняющих контекст), я отказываюсь вставлять ее в эту ветку (что заставило бы ее заметно растягивать страницу). Для получения подробной информации, пожалуйста, перейдите прямо к Gist .

Вот как выглядит решение, используя ту же идею данных, что и выше (полностью закомментированную версию, доступную здесь ) и реализованную в 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
}

Это пример использования новой пары перечисленных черт, которые я создал (находится в этой Gist ), для реализации всех возможностей, желательных и описанных в определении перечисления.

Выражается одно беспокойство: имена членов перечисления должны повторяться ( decorationOrderedSetв приведенном выше примере). Хотя я свел его к минимуму до одного повтора, я не мог понять, как сделать это еще меньше, из-за двух проблем:

  1. Инициализация объекта / класса JVM для этой конкретной модели объекта / случая не определена (см. Этот поток Stackoverflow )
  2. Содержимое, возвращаемое методом, getClass.getDeclaredClassesимеет неопределенный порядок (и вряд ли он будет в том же порядке, что и case objectобъявления в исходном коде)

Учитывая эти две проблемы, мне пришлось отказаться от попыток сгенерировать неявное упорядочение, и мне пришлось явно требовать, чтобы клиент определил и объявил его с каким-то понятием упорядоченного набора. Поскольку коллекции Scala не имеют реализации с упорядоченным набором вставок, лучшее, что я мог сделать, - это использовать Listи затем проверить во время выполнения, что это действительно набор. Это не то, как я бы предпочел добиться этого.

А с учетом конструкции требуется этот второй список / набор упорядоченность val, учитывая ChessPiecesEnhancedDecoratedприведенный выше пример, можно было добавить , case object PAWN2 extends Memberа затем забудьте добавить Decoration(PAWN2,'P2', 2)к decorationOrderedSet. Итак, есть проверка во время выполнения, чтобы убедиться, что список не только набор, но и содержит ВСЕ объекты case, которые расширяют sealed trait Member. Это была особая форма отражения / макро-ада, через которую нужно проработать.


Пожалуйста, оставляйте комментарии и / или отзывы о Gist .

chaotic3quilibrium
источник
Теперь я выпустил первую версию библиотеки ScalaOlio (GPLv3), которая содержит более современные версии обоих org.scalaolio.util.Enumerationи org.scalaolio.util.EnumerationDecorated: scalaolio.org
chaotic3quilibrium
И перейти непосредственно к хранилищу ScalaOlio на Github: github.com/chaotic3quilibrium/scala-olio
chaotic3quilibrium
5
Это качественный ответ и многое из него можно извлечь. Спасибо
angabriel
1
Похоже, Одерски хочет обновить Dotty (будущий Scala 3.0) с нативным перечислением. Whoohoo! github.com/lampepfl/dotty/issues/1970
chaotic3quilibrium
62

Объекты Case уже возвращают свое имя для своих методов toString, поэтому передавать его отдельно не нужно. Вот версия, похожая на jho (удобные методы для краткости опущены):

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

Объекты ленивы; используя вместо этого vals, мы можем удалить список, но должны повторить имя:

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

Если вы не возражаете против обмана, вы можете предварительно загрузить значения перечисления, используя API отражений или что-то вроде Google Reflections. Не ленивые объекты case дают вам самый чистый синтаксис:

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
}

Хороший и чистый, со всеми преимуществами case-классов и перечислений Java. Лично я определяю значения перечисления вне объекта, чтобы лучше соответствовать идиоматическому коду Scala:

object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
GatesDA
источник
3
один вопрос: последнее решение называется «объекты без ленивости», но в этом случае объекты не загружаются, пока мы их не используем: почему вы называете это решение не ленивым?
Себ Цесброн
2
@ Noel, вам нужно использовать: вставить, чтобы вставить всю запечатанную иерархию в REPL. Если вы этого не сделаете, единственная строка с запечатанным базовым классом / признаком считается одним файлом, немедленно запечатывается и не может быть расширена на следующей строке.
Юрген Штробель
2
@GatesDA Только в вашем первом фрагменте кода нет ошибки (поскольку вы явно требуете, чтобы клиент объявлял и определял значения. Как у вашего второго, так и у третьего решения есть небольшая ошибка, которую я описал в своем последнем комментарии (если клиент получает доступ к Currency .GBP и сначала список значений будет «не в порядке».) Я подробно изучил область перечисления Scala и подробно рассказал об этом в своем ответе на эту тему: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
1
Возможно, один из недостатков этого подхода (по сравнению с Java Enums в любом случае) заключается в том, что при вводе валюты <точка> в IDE он не отображает доступные параметры.
Иван Балашов
1
Как упомянул @SebCesbron, объекты case здесь ленивы. Поэтому, если я позвоню Currency.values, я получу только те значения, к которым ранее обращался. Есть ли способ обойти это?
Сасгорилла
27

Преимущества использования case-классов перед перечислениями:

  • При использовании запечатанных падежных классов компилятор Scala может определить, полностью ли найдено совпадение, например, когда все возможные совпадения указаны в объявлении сопоставления. С перечислениями компилятор Scala не может сказать.
  • Классы Case, естественно, поддерживают больше полей, чем перечисление на основе значений, которое поддерживает имя и идентификатор.

Преимущества использования перечислений вместо case-классов:

  • Перечисления, как правило, будут содержать немного меньше кода для записи.
  • Перечисления немного понятнее для новичка в Scala, поскольку они распространены на других языках.

В общем, если вам нужен список простых констант по имени, используйте перечисления. В противном случае, если вам нужно что-то более сложное или вам нужна дополнительная безопасность компилятора, сообщающего вам, указаны ли все совпадения, используйте case-классы.

Аарон
источник
15

ОБНОВЛЕНИЕ: код ниже имеет ошибку, описанную здесь . Тестовая программа, представленная ниже, работает, но если бы вы использовали DayOfWeek.Mon (например) перед самим DayOfWeek, она потерпела бы неудачу, поскольку DayOfWeek не был инициализирован (использование внутреннего объекта не приводит к инициализации внешнего объекта). Вы по-прежнему можете использовать этот код, если вы делаете что-то подобное val enums = Seq( DayOfWeek )в своем основном классе, вызывая инициализацию своих перечислений, или можете использовать модификации chaotic3quilibrium. С нетерпением ждем перечисления на основе макросов!


Если вы хотите

  • предупреждения о неисчерпывающих совпадениях
  • Int ID присваивается каждому значению перечисления, которым вы можете управлять
  • неизменный список значений перечисления в порядке их определения
  • неизменяемая карта от имени до значения enum
  • неизменяемая карта от id до значения enum
  • места для размещения методов / данных для всех или определенных значений перечисления, или для перечисления в целом
  • упорядоченные значения перечисления (так что вы можете проверить, например, день <среда)
  • возможность расширять одно перечисление для создания других

тогда может представлять интерес следующее. Обратная связь приветствуется.

В этой реализации есть абстрактные базовые классы Enum и EnumVal, которые вы расширяете. Мы увидим эти классы через минуту, но сначала вот как вы определите перечисление:

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

Обратите внимание, что вы должны использовать каждое значение перечисления (вызвать его метод apply), чтобы воплотить его в жизнь. [Я бы хотел, чтобы внутренние объекты не были ленивыми, если я специально не попросил их об этом. Думаю.]

Конечно, мы можем добавить методы / данные в DayOfWeek, Val или отдельные объекты case, если мы того пожелаем.

А вот как бы вы использовали такое перечисление:

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

}

Вот что вы получите, когда его скомпилируете:

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

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

Вы можете заменить «день совпадения» на «(день: @unchecked) совпадение», если вы не хотите таких предупреждений, или просто включить в конец универсальный случай.

Когда вы запускаете вышеуказанную программу, вы получаете такой вывод:

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

Обратите внимание, что поскольку List и Maps являются неизменяемыми, вы можете легко удалять элементы для создания подмножеств, не нарушая сам перечисление.

Вот сам класс Enum (и EnumVal внутри него):

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

}

А вот более расширенное использование этого, которое контролирует идентификаторы и добавляет данные / методы к абстракции Val и к самому перечислению:

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
источник
Tyvm за предоставление этого. Я очень ценю это. Тем не менее, я заметил, что он использует «var», а не val. И это пограничный смертный грех в мире FP. Итак, есть ли способ реализовать это так, чтобы не использовать var? Просто любопытно, если это какой-то крайний случай типа FP, и я не понимаю, почему ваша реализация является нежелательной.
chaotic3quilibrium
2
Я, вероятно, не могу вам помочь. В Scala довольно распространено написание классов, которые видоизменяются внутри, но являются неизменными для тех, кто их использует. В приведенном выше примере пользователь DayOfWeek не может изменить перечисление; например, невозможно изменить идентификатор вторника или его имя по факту. Но если вы хотите реализацию , которая свободна от мутации внутри , то я ничего не имею. Однако я не удивлюсь, увидев в 2.11 замечательную новую возможность enum, основанную на макросах; идеи бьют по скала-лангу.
AmigoNico
Я получаю странную ошибку в Scala Worksheet. Если я напрямую использую один из экземпляров Value, я получаю ошибку инициализации. Тем не менее, если я сделаю вызов метода .values, чтобы увидеть содержимое перечисления, это работает, а затем непосредственно с использованием экземпляра значения работает. Любая идея, что ошибка инициализации? И какой оптимальный способ обеспечить инициализацию происходит в правильном порядке независимо от соглашения о вызовах?
chaotic3quilibrium
@ Chaotic3quilibrium: Ух ты! Спасибо за это, и, конечно, спасибо Рекс Керр за тяжелую работу. Я упомяну проблему здесь и ссылаюсь на вопрос, который вы создали.
AmigoNico
«[Использование var] является пограничным смертным грехом в мире FP» - я не думаю, что это мнение общепризнанно.
Эрик Каплун
12

У меня есть хорошая простая библиотека, которая позволяет вам использовать запечатанные черты / классы в качестве значений перечисления без необходимости вести собственный список значений. Он опирается на простой макрос, который не зависит от ошибкиknownDirectSubclasses .

https://github.com/lloydmeta/enumeratum

lloydmeta
источник
10

Обновление за март 2017: как прокомментировал Энтони Акчиоли , scala.Enumeration/enumпиар был закрыт.

Дотти (компилятор следующего поколения для Scala) возглавит дотти выпуск 1970 года и PR Мартина Одерского 1958 года .


Примечание: в настоящее время (август 2016 г., 6+ лет спустя) есть предложение удалить scala.Enumeration : PR 5352

Устаревший scala.Enumeration, добавить @enumаннотацию

Синтаксис

@enum
 class Toggle {
  ON
  OFF
 }

Это возможный пример реализации, намерение также состоит в том, чтобы поддерживать ADT, которые соответствуют определенным ограничениям (без вложенности, рекурсии или изменяющихся параметров конструктора), например:

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

Устаревает безудержная катастрофа, которая есть scala.Enumeration.

Преимущества @enum перед scala.Enumeration:

  • На самом деле работает
  • Java-взаимодействие
  • Нет проблем с удалением
  • Не нужно вводить в заблуждение мини-DSL для изучения при перечислении

Недостатки: нет.

Это решает проблему невозможности иметь одну кодовую базу, которая поддерживает Scala-JVM Scala.jsи Scala-Native (исходный код Java не поддерживается Scala.js/Scala-Native, исходный код Scala не способен определять перечисления, которые принимаются существующими API-интерфейсами в Scala-JVM).

VonC
источник
PR выше закрылся (без радости). Сейчас 2017 год и похоже, что Дотти наконец-то получит конструкцию enum. Вот проблема и пиар Мартина . Слияние, слияние, слияние!
Энтони
8

Еще один недостаток классов case по сравнению с перечислениями, когда вам нужно будет выполнять итерации или фильтровать все экземпляры. Это встроенная возможность перечисления (и перечислений Java), в то время как case-классы автоматически не поддерживают такую ​​возможность.

Другими словами: «нет простого способа получить список полного набора перечисляемых значений с помощью case-классов».

user142435
источник
5

Если вы серьезно относитесь к поддержанию взаимодействия с другими языками JVM (например, Java), тогда лучшим вариантом будет написать перечисления Java. Они работают прозрачно как из кода Scala, так и из кода Java, что больше, чем можно сказать, для scala.Enumerationобъектов case. Давайте не будем иметь новую библиотеку перечислений для каждого нового хобби-проекта на GitHub, если этого можно избежать!

Коннор Дойл
источник
4

Я видел разные версии создания case-класса, имитирующего перечисление. Вот моя версия:

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

Что позволяет вам создавать классы case, которые выглядят следующим образом:

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

Возможно, кто-то может придумать лучший трюк, чем просто добавить класс каждого случая в список, как я. Это было все, что я мог придумать в то время.

Jho
источник
Почему два отдельных неприменимых метода?
Saish
@jho Я пытался проработать ваше решение как есть, но оно не скомпилируется. Во втором фрагменте кода есть ссылка на сайт в "type V = Site". Я не уверен, к чему это относится, чтобы устранить ошибку компиляции. Далее, почему вы предоставляете пустые скобки для «абстрактного класса Currency»? Разве они не могут быть просто остановлены? Наконец, почему вы используете переменную "var values ​​= ..."? Не означает ли это, что клиенты могут в любой момент в любом месте кода назначить новый список значениям? Разве не было бы гораздо предпочтительнее сделать его val вместо var?
chaotic3quilibrium
2

Я ходил туда-сюда по этим двум вариантам последние несколько раз, когда они мне были нужны. До недавнего времени я предпочитал опцию «Запечатанный объект / черта».

1) Счетная декларация Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Запечатанные Черты + Объекты Case

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Хотя ни один из них на самом деле не соответствует всем перечисленным Java-перечислениям, ниже приведены плюсы и минусы:

Scala Enumeration

Плюсы: -Функции для создания экземпляров с опцией или непосредственного предположения о точности (проще при загрузке из постоянного хранилища) -Изменение всех возможных значений поддерживается

Минусы: - Предупреждение о компиляции для неполного поиска не поддерживается (делает сопоставление с шаблоном менее идеальным)

Объекты Case / Запечатанные черты

Плюсы: -Используя запечатанные черты, мы можем предварительно создать некоторые значения, в то время как другие могут быть введены во время создания -Полная поддержка для сопоставления с образцом (определены методы применения / отмены применения)

Минусы: - Создание экземпляров из постоянного хранилища - здесь часто приходится использовать сопоставление с образцом или определять собственный список всех возможных «значений перечисления».

Что в конечном итоге заставило меня изменить свое мнение, было что-то вроде следующего фрагмента:

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
}

Эти .getвызовы были отвратительны - с помощью перечисления вместо этого я могу просто вызвать метод withName на перечисление следующим образом :

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

Поэтому я думаю, что в будущем я предпочитаю использовать Enumerations, если значения предназначены для доступа из репозитория, а объекты case / запечатанные признаки - иначе.

Бешеный пес
источник
Я вижу, насколько желателен второй шаблон кода (избавление от двух вспомогательных методов из первого шаблона кода). Тем не менее, я нашел способ, которым вы не обязаны выбирать между этими двумя шаблонами. Я покрываю весь домен в ответе, который я разместил в этой теме: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
2

Я предпочитаю case objects(это вопрос личных предпочтений). Чтобы справиться с проблемами, присущими этому подходу (анализ строки и итерация по всем элементам), я добавил несколько строк, которые не идеальны, но эффективны.

Я вставляю вам код, ожидая, что он может быть полезен, а также чтобы другие могли его улучшить.

/**
 * 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
источник
0

Для тех, кто все еще ищет, как заставить работать ответ GatesDa : Вы можете просто сослаться на объект case после объявления его, чтобы создать его экземпляр:

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-лампа
источник
0

Я думаю , что самое большое преимущество иметь case classesболее enumerations, что вы можете использовать шаблон класса типа аки однорангового полиморфизма . Не нужно сопоставлять перечисления как:

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

вместо этого у вас будет что-то вроде:

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() = ...
}
Мурат Мустафин
источник