Как моделировать типы enum-типов?

311

В Scala нет безопасных типов, enumкак в Java. Учитывая набор связанных констант, как лучше всего представить в Scala эти константы?

Jesper
источник
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
  }
skaffman
источник
2
Серьезно, приложение не должно использоваться. Это НЕ было исправлено; был представлен новый класс App, в котором нет проблем, упомянутых Шильдмейером. Так что "object foo extends App {...}" И у вас есть немедленный доступ к аргументам командной строки через переменную args.
AmigoNico
scala.Enumeration (то, что вы используете в приведенном выше примере кода «Object WeekDay») не предлагает исчерпывающего сопоставления с образцом. Я исследовал все различные шаблоны перечисления, которые в настоящее время используются в Scala, и дал и рассмотрю их в этом ответе StackOverflow (включая новый шаблон, который предлагает лучшее из scala.Enumeration и шаблона «запечатанный trait + case object»: stackoverflow. com / a / 25923651/501113
chaotic3quilibrium
377

Я должен сказать, что пример, скопированный из документации Scala skaffman выше, на практике имеет ограниченную полезность (вы также можете использовать case objects).

Чтобы получить нечто, наиболее похожее на Java Enum(т. Е. С разумными toStringи valueOfметодами - возможно, вы сохраняете значения enum в базе данных), вам нужно немного его изменить. Если вы использовали код Скаффмана :

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
oxbow_lakes
источник
7
Btw. Метод valueOf теперь мертв :-(
greenoldman
36
Замена @macias valueOf- это withName, который не возвращает Option и выдает NSE, если совпадений нет. Что за!
Bluu
6
@Bluu Вы можете добавить valueOf самостоятельно: def valueOf (name: String) = WeekDay.values.find (_. ToString == name), чтобы иметь Option
центр
@centr Когда я пытаюсь создать Map[Weekday.Weekday, Long]и добавить к нему значение, скажем, Monкомпилятор выдает ошибку недопустимого типа. Ожидаемый день недели. Выходной день нашел значение? Почему это происходит?
Сохаиб
@Sohaib Это должна быть карта [Weekday.Value, Long].
центр
99

Есть много способов сделать.

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 вместо case-объектов. На самом деле, объекты case имеют многократные преимущества, например, здесь. Класс Enumeration, тем не менее, имеет много методов Collection, таких как элементы (итератор в Scala 2.8), который возвращает Iterator, map, flatMap, filter и т. Д.

Этот ответ по сути выбранных частей из этой статьи в моем блоге.

Даниэль С. Собрал
источник
«... не принимаю не-символы там, где ожидается символ»> Я предполагаю, что вы имеете в виду, что Symbolэкземпляры не могут иметь пробелов или специальных символов. Большинство людей, когда впервые встречаются с Symbolклассом, вероятно, думают так, но на самом деле это неправильно. Symbol("foo !% bar -* baz")компилируется и работает отлично. Другими словами, вы можете прекрасно создавать Symbolэкземпляры, обертывающие любую строку (вы просто не можете сделать это с помощью синтаксического сахара "одиночная кома"). Единственное, что Symbolгарантирует, - это уникальность любого данного символа, что делает его намного быстрее для сравнения и сопоставления.
Режис Жан-Жиль
@ RégisJean-Gilles Нет, я имею в виду, что вы не можете передать String, например, аргумент Symbolпараметра.
Даниэль С. Собрал
Да, я понял эту часть, но это довольно спорный вопрос, если вы замените его Stringдругим классом, который в основном является оберткой вокруг строки и может быть свободно преобразован в обоих направлениях (как в случае Symbol). Я думаю, это то, что вы имели в виду, когда говорили «Это не даст вам никакой безопасности типов», просто это было не очень ясно, поскольку OP явно попросил решения по безопасности типов. Я не был уверен, что на момент написания статьи вы знали, что он не только не является безопасным типом, потому что они вообще не являются перечислениями, но также Symbol не гарантирует, что переданный аргумент не будет иметь специальных символов.
Режис Жан-Жиль
1
Для уточнения, когда вы говорите «не принимать не-символы там, где ожидается символ», его можно прочитать либо как «не принимающие значения, которые не являются экземплярами символа» (что, очевидно, верно), либо «не принимающие значения, которые не являются обычный идентификатор, как строки, так называемый "символы» (что не является правдой, и это ошибочное мнение , что в значительной степени у кого - то в первый раз , когда мы сталкиваемся SCALA символов, в связи с тем , что первая встреча является хотя специальной 'fooнотации , которая делает исключающие неидентифицирующие строки). Именно это заблуждение я хотел развеять для любого будущего читателя.
Режис Жан-Жиль
@ 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
На первый взгляд это выглядит чище, но его недостатком является то, что сопровождающему приходится поддерживать синхронизацию одера обоих списков. Для примера дней недели это не кажется вероятным. Но в целом, новое значение может быть вставлено или одно удалено, и два списка могут быть не синхронизированы, и в этом случае могут появиться незначительные ошибки.
Брент Фауст
1
Согласно предыдущему комментарию, риск состоит в том, что два разных списка могут молча не синхронизироваться. Хотя это не проблема для вашего текущего небольшого примера, если есть еще много участников (например, от десятков до сотен), шансы двух списков, которые молча не синхронизируются, значительно выше. Также scala.Enumeration не может извлечь выгоду из исчерпывающих предупреждений / ошибок соответствия шаблонов во время компиляции 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
7

только что обнаружил перечисление . это довольно удивительно и столь же удивительно, это не более известно!

practechal
источник
2

После тщательного изучения всех вариантов «перечислений» в Scala я опубликовал гораздо более полный обзор этого домена в другом потоке StackOverflow . Он включает в себя решение шаблона «запечатанный trait + case object», где я решил проблему упорядочения инициализации класса / объекта JVM.

chaotic3quilibrium
источник
1

У Dotty (Scala 3) будут поддерживаться собственные перечисления. Проверьте здесь и здесь .

zeronone
источник
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)
Дмитрий Кузькин
источник