Як моделювати типи безпечних для переліку типів?


311

У Scala немає безпечних типів enum, як у Java. Враховуючи набір пов'язаних констант, який би кращий спосіб у Скалі представити ці константи?


2
Чому б не просто використовувати java enum? Це одна з небагатьох речей, які я все ще вважаю за краще використовувати звичайну Java.
Макс

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

Відповіді:


187

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

Приклад використання

  object Main extends App {

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

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

    WeekDay.values filter isWorkingDay foreach println
  }

2
Серйозно, додаток не слід використовувати. Це НЕ було зафіксовано; був представлений новий клас, App, який не має проблем, про які згадував Шильдмейєр. Так само "об’єкт foo розширює додаток {...}". У вас є негайний доступ до аргументів командного рядка через змінну args.
AmigoNico

scala. Перерахування (що ви використовуєте у вашому зразку коду "об’єкт WeekDay" вище) не пропонує вичерпного зіставлення шаблонів. Я дослідив усі різні моделі перерахування, які зараз використовуються в Scala, і даю та огляд їх у цій відповіді StackOverflow (включаючи новий зразок, який пропонує найкраще як в масштабі . com / a / 25923651/501113
хаотичний3рівноваги

377

Я повинен сказати , що приклад скопійований з документації Scala по skaffman вище має обмежену корисність на практиці (ви можете також використовувати case objects).

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

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

Беручи до уваги використання наступної декларації:

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

Ви отримуєте більш розумні результати:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue

7
Btw. Метод valueOf тепер мертвий :-(
greenoldman

36
Заміна @macias valueOfє withName, яка не повертає Варіант, і кидає NSE, якщо не відповідає. Що за!
Блу

6
@Bluu Ви можете додати valueOf самостійно: def valueOf (ім'я: String) = WeekDay.values.find (_. ToString == ім'я), щоб мати можливість
центр

@centr Коли я намагаюся створити Map[Weekday.Weekday, Long]і додаю Monдо нього значення, компілятор видає помилку помилкового типу. Очікуваний будній день. Знайдений вихідний день? Чому це відбувається?
Sohaib

@Sohaib Це має бути карта [Weekday.Value, Long].
центр

98

Існує багато способів зробити це.

1) Використовуйте символи. Це не дасть вам безпеки будь-якого типу, окрім того, що не приймати несимволи, де символ очікується. Я тут згадую лише про повноту. Ось приклад використання:

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

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

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

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

2) Використання класу Enumeration:

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

або, якщо вам потрібно його серіалізувати або відобразити:

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

Це можна використовувати так:

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

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

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

scala> import Dimension._
import Dimension._

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

На жаль, це не забезпечує врахування всіх матчів. Якби я забув поставити рядок чи стовпець у матч, укладач Scala не попередив би мене. Тож це дає мені певну безпеку, але не стільки, скільки можна отримати.

3) Об'єкти справи:

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

Тепер, якщо я залишу справу на a match, компілятор попередить мене:

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

    what match {
    ^
one warning found

Він використовується майже так само, і навіть не потребує import:

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

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

Тоді ви можете задатися питанням, навіщо взагалі використовувати перерахунок замість об'єктів регістру. Власне кажучи, об'єкти справи мають багато переваг, як, наприклад, тут. Клас Enumeration, однак, має багато методів колекції, таких як елементи (ітератор на Scala 2.8), який повертає ітератор, карту, flatMap, фільтр тощо.

Ця відповідь по суті є вибраною частиною цієї статті в моєму блозі.


"... не приймаючи несимволи, де очікується символ"> Я здогадуюсь, ви маєте на увазі, що в Symbolекземплярах не може бути пробілів чи спеціальних символів. Більшість людей при першій зустрічі з Symbolкласом, ймовірно, так думають, але насправді неправильно. Symbol("foo !% bar -* baz")компілює і працює ідеально. Іншими словами, ви можете чудово створити Symbolекземпляри, що обгортають будь-яку струну (просто не можете це зробити із синтаксичним цукром "єдиної коми"). Єдине, що Symbolгарантує - це унікальність будь-якого даного символу, що робить його незначно швидшим порівняння та порівняння.
Регіс Жан-Жиль

@ RégisJean-Gilles Ні, я маю на увазі, що ви не можете передавати String, наприклад, аргумент Symbolпараметру.
Даніель К. Собрал

Так, я зрозумів цю частину, але це досить суперечливий момент, якщо ви заміните Stringна інший клас, який в основному є обгорткою навколо рядка і може бути вільно перетворений в обох напрямках (як це має місце Symbol). Я думаю, що це ви мали на увазі, кажучи "Це не дасть вам безпеки будь-якого типу", це було не зовсім зрозуміло, враховуючи, що ОП явно запитувала безпечні рішення для типу. Я не був впевнений, що під час написання ви знали, що це не тільки безпечний тип, оскільки вони зовсім не перераховані, але також Symbol навіть не гарантують, що переданий аргумент не матиме спеціальних знаків.
Регіс Жан-Жиль

1
Щоб уточнити, коли ви говорите "не приймає несимволи, де очікується символ", його можна читати як "неприйняття значень, які не є екземплярами символу" (що, очевидно, правда) або "неприйняття значень, які не є прості рядки, подібні до ідентифікатора, він же "символи" "(що не відповідає дійсності, і це неправильне уявлення про те, що майже будь-хто вперше зустрічається з символами" скала "через те, що перша зустріч - це хоч і особлива 'fooпозначення, яка не дозволяє. рядки без ідентифікатора). Це ця помилка, яку я хотів розвіяти для будь-якого майбутнього читача.
Régis Jean-Gilles

@ RégisJean-Gilles Я мав на увазі колишній, той, що, очевидно, правда. Я маю на увазі, це, очевидно, вірно для тих, хто звик статичного набору тексту. Тоді було багато дискусій про порівняльні достоїнства статичних і «динамічного» набору тексту, і багато людей , зацікавлених в Scala прийшов з динамічної типізації тлі, так що я думав , що це НЕ саме собою зрозуміло. Я б навіть не думав робити це зауваження в наш час. Особисто я вважаю, що Символ Скали є потворним і зайвим, і ніколи його не використовую. Я підтримую ваш останній коментар, оскільки це хороший момент.
Даніель К. Собрал

52

Трохи менш багатослівний спосіб оголошення названих перерахувань:

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

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

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


11
На перший погляд це виглядає більш чистим, але має недолік вимагати від підтримки, щоб синхронізувати нерівність обох списків. На прикладі днів тижня це не видається ймовірним. Але загалом, нове значення можна було б вставити, або одне видалити, і два списки не змогли синхронізуватись, і в цьому випадку можуть бути введені тонкі помилки.
Brent Faust

1
За попереднім коментарем, ризик полягає в тому, що два різних списки можуть мовчки вийти з синхронізації. Хоча це не проблема для вашого нинішнього невеликого прикладу, якщо є багато інших членів (наприклад, у десятках до сотень), шанси двох списків, які мовчки виходять із синхронізації, значно вищі. Також scala.Прорахування не може скористатися вичерпним шаблоном складання часу Scala, що відповідає попередженням / помилкам. Я створив відповідь StackOverflow , який містить рішення , яке виконує перевірку виконання , щоб забезпечити два списки залишається в синхронізації: stackoverflow.com/a/25923651/501113
chaotic3quilibrium

17

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

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

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

object Main {

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

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

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

}

Запечатана ознака з корпусними предметами також є можливою.
Ашалінд

2
У шаблоні "запечатані ознаки + об'єкти регістру" є проблеми, про які я детально описуюсь у відповіді StackOverflow. Тим НЕ менше, я зрозуміти, як вирішити всі питання , пов'язані з цією моделлю , яка також покрита в темі: stackoverflow.com/a/25923651/501113
chaotic3quilibrium


2

Провівши великі дослідження всіх варіантів навколо "перерахування" у Scala, я опублікував значно більш повний огляд цього домену в іншій темі StackOverflow . Він включає рішення схеми "запечатаний ознака + об'єкт випадку", де я вирішив завдання впорядкування класу / об'єкта ініціалізації JVM.



1

У Scala це дуже комфортно з https://github.com/lloydmeta/enumeratum

Проект справді хороший із прикладами та документацією

Саме цей приклад з їхніх документів повинен вас зацікавити

import enumeratum._

sealed trait Greeting extends EnumEntry

object Greeting extends Enum[Greeting] {

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

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

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

}

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

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

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

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

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

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

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

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

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

Greeting.withNameLowercaseOnlyOption("hello")
// => res8: Option[Greeting] = Some(Hello)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.