Об'єкти справи проти перерахування в Scala


231

Чи є вказівки щодо найкращої практики щодо використання класів справ (або об'єктів регістру) проти розширення перерахування в Scala?

Вони, здається, пропонують деякі такі ж переваги.


2
Я написав невеликий огляд про scala Перерахування та альтернативи, вам це може бути корисно: pedrorijo.com/blog/scala-enums/
pedrorijo91

1
Дивіться також Scala 3 на основі Dottyenum (для середини 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. A case objectє більш гнучким, ніж enum:

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 over Enumerations є:

  1. Неможливо повторити всі екземпляри "перерахування" . Це, звичайно, так, але на практиці я вважаю надзвичайно рідкісним, що це потрібно.

  2. Неможливо легко встановити дані про збережене значення . Це також справедливо, але, за винятком величезних перерахувань (наприклад, усіх валют), це не має великих витрат.


10
Інша відмінність полягає в тому, що перерахунок перерахування вказується поза полем, тоді як об'ємний аргумент, який базується на enum obviosly ні,
om-nom-nom

1
Ще один момент для об'єктів випадку - якщо ви дбаєте про сумісність Java. Перерахування повертає значення як Enumeration.Value, таким чином 1) вимагає бібліотека масштабування, 2) втрачає фактичну інформацію про тип.
juanmirocks

7
@oxbow_lakes Щодо пункту 1, зокрема цієї частини "... Я на практиці вкрай рідко вважаю, що це потрібно": Мабуть, ви рідко виконуєте багато роботи в інтерфейсі. Це надзвичайно поширений випадок використання; відображення (випадаючого) списку дійсних членів перерахування, з яких потрібно вибрати.
хаотична рівновага

Я не розумію тип предмета, який відповідає trade.ccyв запечатаному прикладі ознаки.
rloth

і не case objectгенерувати більший (~ 4х) кодовий слід, ніж Enumeration? Корисна відмінність особливо для scala.jsпроектів, які потребують невеликого сліду.
ecoe

69

ОНОВЛЕННЯ: Створено нове рішення на основі макросу, яке набагато перевершує рішення, яке я окреслюю нижче. Я настійно рекомендую використовувати це нове рішення на основі макросу . І, схоже, плани Dotty зроблять цей стиль рішення перерахуванням частиною мови. Whoohoo!

Резюме:
Існує три основні схеми спроби відтворення Java Enumв рамках проекту Scala. Два з трьох візерунків; безпосередньо за допомогою Java Enumта scala.Enumerationне здатні включити вичерпне узгодження шаблону Scala. І третя; "запечатаний ознака + об'єкт регістру", робить ..., але має ускладнення ініціалізації класу / об'єкта JVM, що призводить до непослідовної генерації порядкового індексу.

Я створив рішення з двома класами; Перерахування та перерахуванняDecorated , розміщені в цій суті . Я не розміщував код у цій темі, оскільки файл для перерахунку був досить великий (+400 рядків - містить безліч коментарів, що пояснюють контекст реалізації).

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

Для початку давайте переконаємось, що ми працюємо з тієї ж основної ідеї, що таке перерахування. Давайте визначимо перерахування здебільшого з точки зору Enumнаданого у Java 5 (1.5) :

  1. Він містить природно впорядкований закритий набір названих членів
    1. Є фіксована кількість членів
    2. Члени мають природний порядок та чітко індексуються
      • На відміну від сортування, заснованого на деяких критеріальних даних про вроджених членів
    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 - Розмірковуючи за межами Java Enum, було б непогано мати можливість явно використовувати шаблон Скали, що відповідає вичерпності, перевіряючи перерахування

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


B) Використовуючи шаблон " 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 - Розмірковуючи за межами Java Enum, було б непогано мати можливість явно використовувати шаблон Скали, що відповідає вичерпності, перевіряючи перерахування

Знову ж таки для моїх поточних проектів, пункт 7 є критично важливим для того, щоб я міг вирішити питання про час збирання, якщо / коли я або додаю / видалю члени перерахування, або пишу якийсь новий код для роботи з існуючими членами перерахування.


Отже, з огляду на вищенаведене визначення перерахунку, жодне з вищезгаданих трьох рішень не працює, оскільки не забезпечує все, що зазначено у вищенаведеному визначенні перерахування:

  1. Java Enum безпосередньо у змішаному проекті Scala / Java
  2. "запечатана ознака + об'єкти справи"
  3. scala. Перерахування

Кожне з цих рішень може бути врешті перероблене / розширене / відновлене, щоб спробувати покрити деякі недоліки кожного з них. Однак ні Java, Enumні scala.Enumerationрішення не можуть бути достатньо розширені, щоб забезпечити пункт 7. А для моїх власних проектів це одне з найбільш переконливих значень використання закритого типу в Scala. Я настійно віддаю перевагу компіляції попереджень / помилок, щоб вказати, що у мене є розрив / проблема в моєму коді, на відміну від необхідності виводити його з-за винятку / відмови під час виробництва.


У зв'язку з цим я розпочав роботу з case objectконтуром, щоб побачити, чи можу я створити рішення, яке охоплювало б усе вищезазначене визначення. Перший виклик полягав у тому, щоб проникнути через ядро ​​проблеми ініціалізації класу / об’єкта JVM (детально висвітлено в цій публікації StackOverflow ). І я нарешті змогла знайти рішення.

Як моє рішення - дві риси; Перерахування та перерахування декоровано , і оскільки ця Enumerationознака має довжину понад 400 рядків (багато коментарів, що пояснюють контекст), я пересилаю її в цю нитку (що б змусило її значно розтягнути сторінку). Для детальної інформації перейдіть безпосередньо до Суті .

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

Це приклад використання нової пари ознак перерахування, яку я створив (розташований у цьому розділі ) для реалізації всіх бажаних та окреслених можливостей у визначенні перерахування.

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

  1. Ініціалізація об'єктів / класів JVM для даної конкретної моделі об'єкта / випадку не визначена (див. Цю нитку Stackoverflow )
  2. Вміст, повернутий із методу, getClass.getDeclaredClassesмає невизначений порядок (і навряд чи він буде в тому ж порядку, що і case objectдекларації у вихідному коді)

Враховуючи ці два питання, я повинен був відмовитися від спроби створити неявне замовлення, і я повинен був явно вимагати від клієнта визначення та декларації з якимсь впорядкованим набором понять. Оскільки в колекціях Scala немає впорядкованої реалізації набору, найкраще, що я міг зробити, це використовувати a, Listа потім час перевірки, чи справді це набір. Це не так, як я вважав би за краще цього досягти.

А з урахуванням конструкції потрібно цей другий список / набір впорядкованість val, враховуючи ChessPiecesEnhancedDecoratedнаведений вище приклад, можна було додати , case object PAWN2 extends Memberа потім забудьте додати Decoration(PAWN2,'P2', 2)до decorationOrderedSet. Отже, є перевірка виконання, щоб переконатися, що список є не тільки набором, але містить ВСІ об’єкти регістру, які розширюють sealed trait Member. Це була особлива форма рефлексії / макро пекло для пропрацювання.


Будь ласка, залишайте коментарі та / або відгуки про історію .


Зараз я випустив першу версію бібліотеки 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/isissue/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. Неліниві об'єкти випадку дають вам найчистіший синтаксис:

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
}

Приємно і чисто, з усіма перевагами кейсів і перелічень Java. Особисто я визначаю значення перерахунку поза об'єктом, щоб краще відповідати ідіоматичному коду Scala:

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

3
одне питання: останнє рішення називається "об'єкти лінивих випадків", але в цьому випадку об'єкти не завантажуються, поки ми не використаємо їх: чому ви називаєте це рішення не лінивим?
Себ Цесброн

2
@Noel, вам потрібно використовувати: вставити, щоб вставити всю запечатану ієрархію в REPL. Якщо цього немає, то один рядок із запечатаним базовим класом / ознакою вважається одним файлом, опечатується негайно, і не може бути продовжений у наступному рядку.
Юрген Стробель

2
@GatesDA У вашому першому фрагменті коду немає помилки (оскільки ви явно вимагаєте від клієнта декларування та визначення значень. І ваше друге, і третє рішення мають тонку помилку, яку я описав у своєму останньому коментарі (якщо клієнт має доступ до валюти) .GBP безпосередньо і по-перше, список значень "вийшов з ладу"). Я широко дослідив домен перерахування Scala і детально висвітлив його у своїй відповіді на цю ж нитку: stackoverflow.com/a/25923651/501113
хаотичний3рівноваги

1
Можливо, одним із недоліків такого підходу (порівняно з Java Enums у будь-якому випадку) є те, що коли ви вводите Currency <dot> в IDE, він не показує доступних варіантів.
Іван Балашов

1
Як згадував @SebCesbron, об’єкти справи тут ледачі. Тож якщо я телефоную Currency.values, я отримую лише значення, до яких раніше звертався. Чи є щось подібне?
Сасгоріла

27

Переваги використання класів тестів перед перерахуваннями:

  • Використовуючи запечатані класи справ, компілятор Scala може визначити, чи відповідає збіг повністю, наприклад, коли всі можливі збіги відображені в декларації відповідності. З перерахунками компілятор Scala не може сказати.
  • Класи справ, природно, підтримують більше полів, ніж перерахунок на основі значення, який підтримує ім'я та ідентифікатор.

Перевагами використання перерахунків замість класів регістрів є:

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

Тож загалом, якщо вам просто потрібен список простих констант за назвою, використовуйте перерахування. В іншому випадку, якщо вам потрібно щось трохи складніше або ви хочете, щоб компілятор додаткової безпеки повідомив, чи є у вас вказані всі збіги, використовуйте класи справ.


15

ОНОВЛЕННЯ: Код нижче містить помилку, описану тут . Нижче наведена програма тестування працює, але якщо ви використовували DayOfWeek.Mon (наприклад) перед самим DayOfWeek, вона не вдасться, оскільки DayOfWeek не був ініціалізований (використання внутрішнього об'єкта не викликає ініціалізацію зовнішнього об'єкта). Ви все одно можете використовувати цей код, якщо ви робите щось на кшталт val enums = Seq( DayOfWeek )свого основного класу, змушуючи ініціалізувати ваші перерахунки, або ви можете використовувати модифікації хаотичного3-рівноваги. Чекаємо перерахунку на основі макросу!


Якщо хочете

  • попередження про невичерпну відповідність шаблону
  • Ідентифікатор Int, присвоєний кожному значенню enum, яким ви можете додатково керувати
  • незмінний Перелік значень 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()
}

Зауважте, що вам потрібно використовувати кожне значення enum (називати його метод застосування), щоб реалізувати його. [Я хочу, щоб внутрішні об'єкти не лінувалися, якщо я спеціально не прошу їх. Я думаю.]

Ми, звичайно, можемо додати методи / дані до DayOfWeek, Val або окремих об'єктів випадку, якщо цього захочемо.

А ось як би ти скористався такою перерахунком:

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

Ви можете замінити "day match" на "(day: @uncked) match" там, де ви не хочете таких попереджень, або просто включіть випадок "загальний вигляд" в кінці.

Запустивши вищевказану програму, ви отримаєте такий результат:

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

Зауважте, що оскільки Список і Карти незмінні, ви можете легко видалити елементи для створення підмножин, не порушуючи саму перерахунок.

Ось сам клас 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)
}

Тивм за надання цього. Я дійсно ціную це. Однак я помічаю, що він використовує "var" на відміну від val. І це прикордонний смертний гріх у світі ПП. Отже, чи існує спосіб реалізувати це таким чином, щоб не застосовувати var? Цікаво, якщо це якийсь крайовий випадок типу FP, і я не розумію, наскільки ваша реалізація є FP небажаною.
хаотичний3рівноваги

2
Я, мабуть, не можу вам допомогти. У Scala досить часто писати класи, які внутрішньо мутують, але непорушні тим, хто їх використовує. У наведеному вище прикладі користувач DayOfWeek не може мутувати enum; Наприклад, немає можливості змінити ідентифікатор вівторка або його ім'я після факту. Але якщо ви хочете впровадження, яке не мутація внутрішньо , то я нічого не маю. Я би не здивувався, побачивши гарне нове спонсорство, засноване на макросах у 2,11; ідеї виганяють навколо скала-лангу.
АмігоНіко

Я отримую дивну помилку на листі Scala Worksheet. Якщо я безпосередньо використовую один із екземплярів Value, я отримую помилку ініціалізації. Однак, якщо я звертаюся до методу .values, щоб побачити вміст перерахунку, він працює, а потім безпосередньо використовує екземпляр значення. Будь-яка ідея, що таке помилка ініціалізації? І який оптимальний спосіб забезпечити ініціалізацію в належному порядку незалежно від виклику конвенції?
хаотична рівновага

@ chaotic3quilibrium: Нічого собі! Дякую вам за це, і звичайно дякую Рексу Керру за важкий підйом. Я згадаю про проблему тут і торкнусь створеного вами питання.
AmigoNico

"[Використання var] є прикордонним смертним гріхом у світі ПП" - я не думаю, що думка є загальновизнаною.
Ерік Каплун

12

У мене є хороший простий ліб, який дозволяє використовувати запечатані риси / класи як значення перерахунків, не потребуючи ведення власного списку значень. Він спирається на простий макрос, який не залежить від баггі knownDirectSubclasses.

https://github.com/lloydmeta/enumeratum


10

Оновлення березня 2017 року: як коментує Ентоні Аксьолі , scala.Enumeration/enumPR закрито.

Dotty (компілятор наступного покоління для 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. Перерахунок:

  • Насправді працює
  • Java interop
  • Жодних проблем зі стиранням
  • Не заплутаний mini-DSL, щоб дізнатися при визначенні перерахувань

Недоліки: немає.

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


PR вище закрився (немає радості). Зараз 2017 рік і схоже, що Дотті нарешті отримає конструкцію перерахунку. Ось питання і піар Мартіна . Злиття, злиття, злиття!
Ентоні Акціолій

8

Ще один недолік класів справ у порівнянні з перерахуваннями, коли вам потрібно буде повторити чи фільтрувати всі екземпляри. Це вбудована здатність перерахування (і Java перераховує), в той час як класи справ автоматично не підтримують таку можливість.

Іншими словами: "не існує простого способу отримати список загального набору перерахованих значень за допомогою класів".


5

Якщо ви серйозно ставитесь до збереження сумісності з іншими мовами JVM (наприклад, Java), то найкращим варіантом є написання переліків Java. Вони працюють прозоро як від Scala, так і від Java-коду, що більше, ніж можна сказати, scala.Enumerationабо для об'єктів case. Не будемо мати нової бібліотеки перерахунків для кожного нового проекту хобі на GitHub, якщо цього можна уникнути!


4

Я бачив різні версії того, щоб зробити клас класу, що імітує перерахування. Ось моя версія:

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

Що дозволяє побудувати класи справ, які виглядають так:

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 Я намагався працювати над вашим рішенням як є, але він не збирається. У другому фрагменті коду є посилання на Сайт у "тип V = Сайт". Я не впевнений, на що йдеться, щоб усунути помилку компіляції. Далі, чому ви надаєте порожні дужки для "валюти абстрактного класу"? Чи не могли вони просто залишитися? Нарешті, чому ви використовуєте var у "var values ​​= ..."? Чи це не означає, що клієнти могли в будь-який час з будь-якої точки коду призначити новий список значень? Чи не було б набагато кращим зробити його валом замість вару?
хаотичний3рівноваги

2

Я повертався вперед і назад на ці два варіанти останні кілька разів, коли мені були потрібні. До недавнього часу моїм уподобанням був варіант із запечатаною ознакою / об'єктом справи.

1) Декларація перерахування Scala

object OutboundMarketMakerEntryPointType extends Enumeration {
  type OutboundMarketMakerEntryPointType = Value

  val Alpha, Beta = Value
}

2) Опечатані риси + об'єкти справи

sealed trait OutboundMarketMakerEntryPointType

case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType

case object BetaEntryPoint extends OutboundMarketMakerEntryPointType

Хоча жодне з цих дій не відповідає всім тим, що дає вам перелік Java, нижче наведені плюси і мінуси:

Перерахунок Scala

Плюси: -Функції для інстанції з опцією або безпосередньо припущення точних (простіше при завантаженні з постійного магазину) -Ітерація над усіма можливими значеннями підтримується

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

Об'єкти справи / Опечатані ознаки

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

Мінуси: - Введення даних із постійного магазину - вам часто доводиться використовувати відповідність шаблонів тут або визначати свій власний список усіх можливих «значень перерахувань»

Що зрештою змусило мене змінити свою думку, було щось на зразок наступного фрагмента:

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

Тому я вважаю, що моїм перевагою вперед є використання перерахунків, коли значення мають бути доступними з сховища та об'єктами регістрів / запечатаними ознаками в іншому випадку.


Я бачу, як бажано другий шаблон коду (позбавлення двох допоміжних методів з першого шаблону коду). Однак я зрозумів, що такий спосіб не змушений вибирати між цими двома моделями. Я весь домен у відповідь я відправив на цю тему: 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 }
}

0

Для тих, хто все ще шукає, як змусити відповісти GatesDa на роботу : Ви можете просто посилатись на об'єкт справи після того, як оголосив його про своє ім'я:

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

Я думаю , що найбільша перевага мати 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() = ...
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.