Bagaimana memodelkan tipe enum yang aman?

311

Scala tidak memiliki tipe-aman enumseperti yang dimiliki Java. Diberikan seperangkat konstanta terkait, apa yang akan menjadi cara terbaik di Scala untuk mewakili konstanta tersebut?

Jesper
sumber
2
Mengapa tidak menggunakan java enum saja? Ini adalah salah satu dari beberapa hal yang saya masih suka menggunakan java polos.
Maks
1
Saya telah menulis ikhtisar kecil tentang pencacahan scala dan alternatif, Anda mungkin menemukan itu berguna: pedrorijo.com/blog/scala-enums/
pedrorijo91

Jawaban:

187

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

Contoh penggunaan

  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
sumber
2
Serius, Aplikasi tidak boleh digunakan. Itu TIDAK diperbaiki; kelas baru, App, diperkenalkan, yang tidak memiliki masalah yang disebutkan oleh Schildmeijer. Begitu juga "object foo extends App {...}" Dan Anda memiliki akses langsung ke argumen baris perintah melalui variabel args.
AmigoNico
scala.Enumeration (yang Anda gunakan dalam contoh kode "object WeekDay" di atas) tidak menawarkan pencocokan pola yang lengkap. Saya telah meneliti semua pola enumerasi yang berbeda yang saat ini sedang digunakan di Scala dan memberikan dan ikhtisar dari mereka dalam jawaban StackOverflow ini (termasuk pola baru yang menawarkan yang terbaik dari kedua scala.Penelitian dan pola "disegel sifat + objek objek" pola: stackoverflow. com / a / 25923651/501113
chaotic3quilibrium
377

Saya harus mengatakan bahwa contoh yang disalin dari dokumentasi Scala oleh skaffman di atas adalah utilitas terbatas dalam praktek (Anda mungkin juga menggunakan case objects).

Untuk mendapatkan sesuatu yang paling mirip dengan Java Enum(yaitu dengan metode yang masuk akal toStringdan valueOf- mungkin Anda mempertahankan nilai enum ke database) Anda perlu sedikit memodifikasinya. Jika Anda menggunakan kode skaffman :

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

Sedangkan menggunakan deklarasi berikut:

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

Anda mendapatkan hasil yang lebih masuk akal:

WeekDay.valueOf("Sun") //returns Some(Sun)
WeekDay.Tue.toString   //returns Tue
oxbow_lakes
sumber
7
Btw. Metode valueOf sekarang mati :-(
greenoldman
36
@macias valueOfpengganti withName, yang tidak mengembalikan Opsi, dan melempar NSE jika tidak ada kecocokan. Apa itu!
Bluu
6
@Bluu Anda dapat menambahkan valueOf sendiri: def valueOf (name: String) = WeekDay.values.find (_. ToString == name) untuk memiliki Opsi
Centang
@ centr Ketika saya mencoba membuat Map[Weekday.Weekday, Long]dan menambahkan nilai katakan Monpadanya kompiler melempar kesalahan jenis tidak valid. Diharapkan hari kerja. Setiap hari nilai ditemukan? Mengapa ini terjadi?
Sohaib
@Sohaib Seharusnya Peta [Weekday.Value, Long].
centr
99

Ada banyak cara untuk melakukannya.

1) Gunakan simbol. Namun, itu tidak akan memberi Anda keamanan jenis apa pun, selain tidak menerima non-simbol di mana simbol diharapkan. Saya hanya menyebutkannya di sini untuk kelengkapan. Berikut ini contoh penggunaannya:

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) Menggunakan kelas Enumeration:

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

atau, jika Anda perlu membuat cerita bersambung atau menampilkannya:

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

Ini dapat digunakan seperti ini:

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 /

Sayangnya, itu tidak memastikan bahwa semua pertandingan dicatat. Jika saya lupa memasukkan Baris atau Kolom dalam pertandingan, kompiler Scala tidak akan memperingatkan saya. Jadi itu memberi saya beberapa jenis keamanan, tetapi tidak sebanyak yang bisa diperoleh.

3) Objek kasing:

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

Sekarang, jika saya meninggalkan kasing pada match, kompiler akan memperingatkan saya:

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

    what match {
    ^
one warning found

Ini digunakan dengan cara yang hampir sama, dan bahkan tidak memerlukan 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 /

Jadi, Anda mungkin bertanya-tanya, mengapa pernah menggunakan Enumerasi alih-alih objek kasus. Faktanya, objek kasus memang memiliki kelebihan berkali-kali, seperti di sini. Kelas Enumerasi, bagaimanapun, memiliki banyak metode Koleksi, seperti elemen (iterator pada Scala 2.8), yang mengembalikan Iterator, peta, flatMap, filter, dll.

Jawaban ini pada dasarnya adalah bagian yang dipilih dari artikel ini di blog saya.

Daniel C. Sobral
sumber
"... tidak menerima non-simbol di mana simbol diharapkan"> Saya menduga Anda berarti bahwa Symbolinstance tidak dapat memiliki spasi atau karakter khusus. Kebanyakan orang ketika pertama kali bertemu Symbolkelas mungkin berpikir begitu, tetapi sebenarnya tidak benar. Symbol("foo !% bar -* baz")kompilasi dan berjalan dengan sangat baik. Dengan kata lain Anda dapat dengan sempurna membuat Symbolinstance yang membungkus string apa pun (Anda tidak bisa melakukannya dengan gula sintaksis "koma tunggal"). Satu-satunya hal yang Symbolmenjamin adalah keunikan simbol yang diberikan, membuatnya sedikit lebih cepat untuk membandingkan dan mencocokkan.
Régis Jean-Gilles
@ RégisJean-Gilles Tidak, maksud saya Anda tidak dapat melewatkan String, misalnya, sebagai argumen ke Symbolparameter.
Daniel C. Sobral
Ya, saya mengerti bagian itu, tetapi itu adalah poin yang cukup bisa diperdebatkan jika Anda mengganti Stringdengan kelas lain yang pada dasarnya adalah pembungkus di sekitar string dan dapat dikonversi secara bebas di kedua arah (seperti halnya untuk Symbol). Saya kira itulah yang Anda maksud ketika mengatakan "Itu tidak akan memberi Anda keamanan jenis apa pun", hanya saja tidak begitu jelas mengingat bahwa OP secara eksplisit meminta solusi jenis aman. Saya tidak yakin apakah pada saat penulisan Anda tahu bahwa bukan saja itu bukan tipe aman karena itu sama sekali bukan enum, tetapi juga Symbol tidak menjamin bahwa argumen yang disahkan tidak akan memiliki karakter khusus.
Régis Jean-Gilles
1
Untuk menguraikan, ketika Anda mengatakan "tidak menerima non-simbol di mana simbol diharapkan", itu dapat dibaca sebagai "tidak menerima nilai yang bukan contoh dari Simbol" (yang jelas benar) atau "tidak menerima nilai yang tidak string seperti identifier, alias 'simbol' "(yang tidak benar, dan merupakan kesalahpahaman bahwa hampir semua orang memiliki pertama kali kita menjumpai simbol scala, karena fakta bahwa pertemuan pertama adalah melalui 'foonotasi khusus yang tidak menghalangi string bukan pengidentifikasi). Inilah kesalahpahaman yang ingin saya singkirkan untuk pembaca di masa depan.
Régis Jean-Gilles
@ RégisJean-Gilles yang saya maksud adalah yang pertama, yang jelas benar. Maksudku, itu jelas benar bagi siapa pun yang terbiasa mengetik statis. Saat itu ada banyak diskusi tentang manfaat relatif dari pengetikan statis dan "dinamis", dan banyak orang yang tertarik pada Scala berasal dari latar belakang pengetikan yang dinamis, jadi saya pikir itu tidak sesuai tanpa mengatakan. Saya bahkan tidak akan berpikir untuk membuat pernyataan itu saat ini. Secara pribadi, saya pikir Scala's Symbol jelek dan berlebihan, dan tidak pernah menggunakannya. Saya membatalkan komentar terakhir Anda, karena itu poin yang bagus.
Daniel C. Sobral
52

Cara mendeklarasikan enumerasi dengan nama yang sedikit kurang jelas:

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

Tentu saja masalahnya di sini adalah bahwa Anda harus menjaga agar urutan nama dan vokal tetap sinkron yang lebih mudah dilakukan jika nama dan val dinyatakan pada baris yang sama.

Walter Chang
sumber
11
Sepintas ini terlihat lebih bersih, tetapi memiliki kerugian karena mengharuskan pengelola untuk menjaga agar kedua daftar tetap sinkron. Untuk contoh hari dalam seminggu, sepertinya tidak mungkin. Tetapi secara umum, nilai baru bisa dimasukkan, atau satu dihapus dan dua daftar bisa tidak sinkron, dalam hal ini, bug halus dapat diperkenalkan.
Brent Faust
1
Per komentar sebelumnya, risikonya adalah dua daftar yang berbeda dapat secara diam-diam tidak sinkron. Meskipun ini bukan masalah bagi contoh kecil Anda saat ini, jika ada lebih banyak anggota (seperti dalam puluhan hingga ratusan), kemungkinan kedua daftar secara diam-diam tidak sinkron secara substansial lebih tinggi. Juga scala.Enumerasi tidak dapat mengambil manfaat dari peringatan / kesalahan pencocokan pola kompilasi waktu lengkap Scala. Saya telah membuat jawaban StackOverflow yang berisi solusi melakukan pemeriksaan runtime untuk memastikan dua daftar tetap sinkron: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17

Anda bisa menggunakan kelas abstrak tersegel alih-alih enumerasi, misalnya:

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

}
ron
sumber
Sifat tertutup dengan objek kasing juga kemungkinan.
Ashalynd
2
Pola "objek sifat case + tersegel" memiliki masalah yang saya detailkan dalam jawaban StackOverflow. Namun, saya menemukan cara untuk menyelesaikan semua masalah yang terkait dengan pola ini yang juga dibahas di utas: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
7

baru saja menemukan enumeratum . itu sangat menakjubkan dan sama-sama luar biasa itu tidak lebih terkenal!

praktikum
sumber
2

Setelah melakukan penelitian ekstensif pada semua opsi di sekitar "enumerasi" di Scala, saya memposting tinjauan yang jauh lebih lengkap dari domain ini pada utas StackOverflow lainnya . Ini termasuk solusi untuk pola "seal trait + case object" di mana saya telah memecahkan masalah pemesanan inisialisasi kelas / objek JVM.

chaotic3quilibrium
sumber
1

Dotty (Scala 3) akan memiliki enum asli yang didukung. Periksa di sini dan di sini .

zeronone
sumber
1

Di Scala sangat nyaman dengan https://github.com/lloydmeta/enumeratum

Project sangat bagus dengan contoh dan dokumentasi

Contoh ini hanya dari dokumen mereka harus membuat Anda tertarik

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)
Dmitriy Kuzkin
sumber