Bagaimana cara mendefinisikan "type disjunction" (tipe union)?

181

Salah satu cara yang disarankan untuk menangani definisi ganda dari metode kelebihan beban adalah mengganti kelebihan beban dengan pencocokan pola:

object Bar {
   def foo(xs: Any*) = xs foreach { 
      case _:String => println("str")
      case _:Int => println("int")
      case _ => throw new UglyRuntimeException()
   }
}

Pendekatan ini mengharuskan kami menyerahkan tipe statis memeriksa argumen untuk foo. Akan jauh lebih baik untuk bisa menulis

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case _: String => println("str")
      case _: Int => println("int")
   }
}

Saya bisa akrab dengan Either, tetapi menjadi cepat jelek dengan lebih dari dua jenis:

type or[L,R] = Either[L,R]

implicit def l2Or[L,R](l: L): L or R = Left(l)
implicit def r2Or[L,R](r: R): L or R = Right(r)

object Bar {
   def foo(xs: (String or Int)*) = xs foreach {
      case Left(l) => println("str")
      case Right(r) => println("int")
   }
}

Sepertinya seorang jenderal (elegan, efisien) solusi akan membutuhkan mendefinisikan Either3, Either4, .... Apakah ada yang tahu dari solusi alternatif untuk mencapai tujuan yang sama? Setahu saya, Scala tidak memiliki "type disjunction" bawaan. Juga, apakah konversi implisit yang didefinisikan di atas bersembunyi di perpustakaan standar di suatu tempat sehingga saya bisa mengimpornya?

Aaron Novstrup
sumber

Jawaban:

142

Nah, dalam kasus khusus Any*, trik di bawah ini tidak akan berfungsi, karena tidak akan menerima tipe campuran. Namun, karena tipe campuran tidak akan bekerja dengan kelebihan beban, ini mungkin yang Anda inginkan.

Pertama, deklarasikan kelas dengan tipe yang ingin Anda terima seperti di bawah ini:

class StringOrInt[T]
object StringOrInt {
  implicit object IntWitness extends StringOrInt[Int]
  implicit object StringWitness extends StringOrInt[String]
}

Selanjutnya, nyatakan fooseperti ini:

object Bar {
  def foo[T: StringOrInt](x: T) = x match {
    case _: String => println("str")
    case _: Int => println("int")
  }
}

Dan itu saja. Anda dapat menelepon foo(5)atau foo("abc"), dan itu akan berhasil, tetapi cobalah foo(true)dan itu akan gagal. Ini bisa menjadi sisi-melangkah dengan kode klien dengan menciptakan StringOrInt[Boolean], kecuali, seperti dicatat oleh Randall bawah, Anda membuat StringOrIntsebuah sealedkelas.

Ini bekerja karena T: StringOrIntberarti ada parameter tipe implisit StringOrInt[T], dan karena Scala melihat ke dalam objek pendamping tipe untuk melihat apakah ada implisit di sana untuk membuat kode yang meminta tipe itu bekerja.

Daniel C. Sobral
sumber
14
Jika class StringOrInt[T]dibuat sealed, "kebocoran" yang Anda maksudkan ("Tentu saja, ini bisa digerakkan oleh kode klien dengan membuat StringOrInt[Boolean]") dicolokkan, setidaknya jika StringOrIntberada di file sendiri. Maka objek saksi harus didefinisikan dalam sumber yang sama dengan StringOrInt.
Randall Schulz
3
Saya mencoba menggeneralisasi solusi ini (diposting sebagai jawaban di bawah). Kelemahan utama dibandingkan dengan Eitherpendekatan tampaknya adalah bahwa kami kehilangan banyak dukungan kompiler untuk memeriksa pertandingan.
Aaron Novstrup
trik yang bagus! Namun, bahkan dengan kelas yang disegel, Anda masih bisa mengelak dalam kode klien dengan mendefinisikan val implisit b = new StringOrInt [Boolean] dalam lingkup dengan foo, atau dengan memanggil secara eksplisit foo (2.9) (StringOrInt [Double] baru). Saya pikir Anda perlu membuat kelas abstrak juga.
Paolo Falabella
2
Iya; mungkin akan lebih baik untuk menggunakantrait StringOrInt ...
Siput mekanik
7
Ps jika Anda ingin mendukung subtipe cukup ubah StringOrInt[T]ke StringOrInt[-T](lihat stackoverflow.com/questions/24387701/… )
Eran Medan
178

Miles Sabin menjelaskan cara yang sangat bagus untuk mendapatkan jenis serikat di posting blog terbarunya Jenis serikat tanpa kotak di Scala melalui isomorfisma Curry-Howard :

Dia pertama-tama mendefinisikan negasi jenis sebagai

type ¬[A] = A => Nothing

menggunakan hukum De Morgan, ini memungkinkannya untuk menentukan jenis serikat pekerja

type[T, U] = ¬[¬[T] with ¬[U]]

Dengan konstruksi bantu berikut

type ¬¬[A] = ¬[¬[A]]
type ||[T, U] = { type λ[X] = ¬¬[X] <:< (T ∨ U) }

Anda dapat menulis jenis serikat sebagai berikut:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
sumber
13
Itu adalah salah satu hal paling menakjubkan yang pernah saya lihat.
Submonoid
18
Ini adalah implementasi lanjutan dari ide Miles saya: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - dengan contoh: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ test /…
Connor Doyle
6
Komentar di atas harus menjadi jawaban sendiri. Itu hanya implementasi dari ide Miles, tetapi dibungkus dengan baik dalam sebuah paket di Maven Central, dan tanpa semua simbol unicode yang mungkin (?) Menimbulkan masalah untuk sesuatu dalam proses pembangunan di suatu tempat.
Jim Pivarski
2
Karakter lucu itu adalah negasi boolean .
michid
1
Awalnya, gagasan itu tampak terlalu berbelit-belit bagi saya. Membaca hampir setiap tautan yang disebutkan di utas ini, saya terperangkap oleh gagasan dan keindahan penerapannya :-) ... tapi saya masih merasa ini adalah sesuatu yang berbelit-belit ... sekarang hanya karena belum tersedia secara langsung jauh dari Scala. Seperti yang dikatakan Miles: "Sekarang kita hanya perlu mengganggu Martin dan Adriaan agar dapat diakses secara langsung."
Richard Gomes
44

Dotty , kompiler Scala eksperimental baru, mendukung tipe union (tertulis A | B), sehingga Anda dapat melakukan apa yang Anda inginkan:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Samuel Gruetter
sumber
1
Suatu hari.
Michael Ahlers
5
Ngomong-ngomong, Dotty akan menjadi scala 3 baru (diumumkan beberapa bulan yang lalu).
6infinity8
1
dan akan tersedia di suatu tempat pada akhir 2020
JulienD
31

Berikut adalah cara Rex Kerr untuk menyandikan tipe serikat. Lurus dan sederhana!

scala> def f[A](a: A)(implicit ev: (Int with String) <:< A) = a match {
     |   case i: Int => i + 1
     |   case s: String => s.length
     | }
f: [A](a: A)(implicit ev: <:<[Int with String,A])Int

scala> f(3)
res0: Int = 4

scala> f("hello")
res1: Int = 5

scala> f(9.2)
<console>:9: error: Cannot prove that Int with String <:< Double.
       f(9.2)
        ^

Sumber: Komentar # 27 di bawah posting blog yang sangat baik ini oleh Miles Sabin yang menyediakan cara lain untuk meng-encode jenis-jenis serikat di Scala.

missingfaktor
sumber
6
Sayangnya, pengodean ini dapat dikalahkan: scala> f(9.2: AnyVal)melewati pengetikan.
Kipton Barros
@ Cipton: Menyedihkan. Apakah penyandian Miles Sabin juga mengalami masalah ini?
missingfaktor
9
Ada versi kode Miles yang sedikit lebih sederhana; karena dia benar-benar menggunakan implikasi sebaliknya dari parameter contravariant dari fungsi, bukan "tidak" yang ketat, Anda dapat menggunakan trait Contra[-A] {}semua fungsi untuk menggantikannya. Jadi Anda mendapatkan barang-barang seperti type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }bekas seperti def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(tanpa mewah unicode).
Rex Kerr
Ini mungkin memecahkan masalah warisan jenis serikat? stackoverflow.com/questions/45255270/…
jhegedus
Hmm, saya mencobanya, saya tidak bisa membuat tipe pengembalian dengan pengkodean ini, jadi sepertinya tidak mungkin untuk mengimplementasikan subtyping stackoverflow.com/questions/45255270/…
jhegedus
18

Dimungkinkan untuk menggeneralisasi solusi Daniel sebagai berikut:

sealed trait Or[A, B]

object Or {
   implicit def a2Or[A,B](a: A) = new Or[A, B] {}
   implicit def b2Or[A,B](b: B) = new Or[A, B] {}
}

object Bar {
   def foo[T <% String Or Int](x: T) = x match {
     case _: String => println("str")
     case _: Int => println("int")
   }
}

Kelemahan utama dari pendekatan ini adalah

  • Seperti yang ditunjukkan Daniel, ini tidak menangani koleksi / vararg dengan tipe campuran
  • Kompiler tidak mengeluarkan peringatan jika pertandingan tidak lengkap
  • Kompiler tidak mengeluarkan kesalahan jika pertandingan menyertakan case yang mustahil
  • Seperti Eitherpendekatan, generalisasi lebih lanjut akan membutuhkan mendefinisikan analog Or3, Or4, dll sifat. Tentu saja, mendefinisikan sifat-sifat seperti itu akan jauh lebih sederhana daripada mendefinisikan Eitherkelas yang sesuai .

Memperbarui:

Mitch Blevins menunjukkan pendekatan yang sangat mirip dan menunjukkan bagaimana menggeneralisasikannya ke lebih dari dua jenis, menyebutnya "gagap atau".

Aaron Novstrup
sumber
18

Saya agak tersandung pada implementasi yang relatif bersih dari tipe-tipe serikat pekerja dengan menggabungkan konsep daftar tipe dengan penyederhanaan pekerjaan Miles Sabin di area ini , yang seseorang sebutkan dalam jawaban lain.

Jenis ¬[-A]yang diberikan bersifat contravarian A, dengan definisi yang diberikan A <: Bkita dapat menulis ¬[B] <: ¬[A], membalik urutan jenis.

Jenis diberikan A, Bdan X, kami ingin mengungkapkan X <: A || X <: B. Kami menerapkan contravariance ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Kaleng pada gilirannya ini dinyatakan sebagai ¬[A] with ¬[B] <: ¬[X]yang salah satu Aatau Bharus menjadi supertype dari Xatau Xsendiri (berpikir tentang argumen fungsi).

object Union {
  import scala.language.higherKinds

  sealed trait ¬[-A]

  sealed trait TSet {
    type Compound[A]
    type Map[F[_]] <: TSet
  }

  sealed traitextends TSet {
    type Compound[A] = A
    type Map[F[_]] =}

  // Note that this type is left-associative for the sake of concision.
  sealed trait[T <: TSet, H] extends TSet {
    // Given a type of the form `∅ ∨ A ∨ B ∨ ...` and parameter `X`, we want to produce the type
    // `¬[A] with ¬[B] with ... <:< ¬[X]`.
    type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

    // This could be generalized as a fold, but for concision we leave it as is.
    type Compound[A] = T#Compound[H with A]

    type Map[F[_]] = T#Map[F] ∨ F[H]
  }

  def foo[A : (∅ ∨ StringIntList[Int])#Member](a: A): String = a match {
    case s: String => "String"
    case i: Int => "Int"
    case l: List[_] => "List[Int]"
  }

  foo(42)
  foo("bar")
  foo(List(1, 2, 3))
  foo(42d) // error
  foo[Any](???) // error
}

Aku menghabiskan waktu berusaha untuk menggabungkan ide ini dengan batas atas jenis anggota seperti yang terlihat di TLists dari harrah / up , namun pelaksanaan Mapdengan jenis batas-batas sejauh ini terbukti menantang.

J Cracknell
sumber
1
Ini brilian, terima kasih! Saya mencoba pendekatan sebelumnya tetapi terus mengalami masalah menggunakan ini dengan tipe generik sebagai bagian dari persatuan. Ini adalah satu-satunya implementasi yang bisa saya kerjakan dengan tipe generik.
Samer Adra
Sedihnya, tetapi mungkin sudah diduga, ketika saya mencoba menggunakan metode Scala yang mengambil tipe gabungan dari kode Java, itu tidak berfungsi. Kesalahan: (40, 29) java: metode setValue di kelas Config tidak dapat diterapkan ke tipe yang diberikan; diperlukan: X, scala.Predef. $ kurang $ usus $ kurang <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> ditemukan: java.lang. Alasan pengerjaan: tidak dapat menyimpulkan type-variable X (panjang daftar argumen aktual dan formal)
Samer Adra
Masih belum cukup jelas tentang beberapa detail dalam implementasi ini. Sebagai contoh, artikel asli mendefinisikan negasi sebagai "type ¬ [A] = A => Nothing" tetapi dalam versi ini jika hanya memiliki "disegel sifat ¬ [-A]" dan sifat tersebut tidak diperpanjang di mana saja. Bagaimana cara kerjanya?
Samer Adra
@Samer Adra Cara ini akan berhasil, artikel ini menggunakan Function1jenis contravarian yang ada. Anda tidak perlu implementasi, yang Anda butuhkan hanyalah bukti kesesuaian ( <:<).
J Cracknell
Adakah yang tahu bagaimana memiliki konstruktor yang menerima tipe serikat pekerja?
Samer Adra
13

Solusi kelas tipe mungkin cara terbaik untuk pergi ke sini, menggunakan implisit. Ini mirip dengan pendekatan monoid yang disebutkan dalam buku Odersky / Spoon / Venners:

abstract class NameOf[T] {
  def get : String
}

implicit object NameOfStr extends NameOf[String] {
  def get = "str"
}

implicit object NameOfInt extends NameOf[Int] {
 def get = "int"
}

def printNameOf[T](t:T)(implicit name : NameOf[T]) = println(name.get)

Jika Anda menjalankan ini di REPL:

scala> printNameOf(1)
int

scala> printNameOf("sss")
str

scala> printNameOf(2.0f)
<console>:10: error: could not find implicit value for parameter nameOf: NameOf[
Float]
       printNameOf(2.0f)

              ^
Kevin Wright
sumber
Saya bisa saja salah, tetapi saya tidak berpikir inilah yang dicari OP. OP bertanya tentang tipe data yang dapat mewakili persatuan tipe yang terpisah, dan kemudian melakukan analisis kasus pada saat runtime untuk melihat apa tipe yang sebenarnya. Tipe class tidak akan menyelesaikan masalah ini, karena mereka adalah konstruk murni waktu kompilasi.
Tom Crockett
5
Pertanyaan sesungguhnya yang diajukan adalah bagaimana mengekspos perilaku yang berbeda untuk tipe yang berbeda, tetapi tanpa kelebihan beban. Tanpa pengetahuan tentang kelas tipe (dan mungkin beberapa paparan C / C ++), tipe serikat tampaknya menjadi satu-satunya solusi. Jenis Scala yang sudah ada Eithercenderung memperkuat keyakinan ini. Menggunakan kelas tipe melalui implisit Scala adalah solusi yang lebih baik untuk masalah yang mendasarinya, tetapi ini adalah konsep yang relatif baru dan masih belum diketahui secara luas, itulah mengapa OP bahkan tidak tahu untuk menganggapnya sebagai alternatif yang mungkin untuk tipe serikat.
Kevin Wright
apakah ini bekerja dengan subtyping? stackoverflow.com/questions/45255270/…
jhegedus
10

Kami ingin operator tipe Or[U,V]yang dapat digunakan untuk membatasi parameter tipe Xsedemikian rupa sehingga dapat X <: Uatau X <: V. Inilah definisi yang sedekat yang bisa kita dapatkan:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Inilah cara penggunaannya:

// use

class A; class B extends A; class C extends B

def foo[X : (B Or String)#pf] = {}

foo[B]      // OK
foo[C]      // OK
foo[String] // OK
foo[A]      // ERROR!
foo[Number] // ERROR!

Ini menggunakan beberapa trik tipe Scala. Yang utama adalah penggunaan batasan tipe umum . Jenis yang diberikan Udan V, kompilator Scala menyediakan kelas yang disebut U <:< V(dan objek implisit dari kelas itu) jika dan hanya jika kompiler Scala dapat membuktikan bahwa itu Uadalah subtipe dari V. Berikut adalah contoh sederhana menggunakan batasan tipe umum yang berfungsi untuk beberapa kasus:

def foo[X](implicit ev : (B with String) <:< X) = {}

Contoh ini berfungsi ketika Xturunan kelas B, a String, atau memiliki tipe yang bukan tipe supert atau subtipe dari Batau String. Dalam dua kasus pertama, itu benar dengan definisi withkata kunci itu (B with String) <: Bdan (B with String) <: String, jadi Scala akan memberikan objek implisit yang akan diteruskan sebagai ev: kompiler Scala akan menerima foo[B]dan dengan benar foo[String].

Dalam kasus terakhir, saya mengandalkan fakta bahwa jika U with V <: X, maka U <: Xatau V <: X. Tampaknya secara intuitif benar, dan saya hanya mengasumsikannya. Dari asumsi ini jelas mengapa contoh sederhana ini gagal ketika Xsupertipe atau subtipe dari salah satu Batau String: misalnya, dalam contoh di atas, foo[A]diterima secara tidak benar dan foo[C]ditolak secara salah. Sekali lagi, apa yang kita inginkan adalah semacam ekspresi tipe pada variabel U, Vdan Xitu benar kapan tepatnya X <: Uatau X <: V.

Gagasan tentang kontradiksi Scala dapat membantu di sini. Ingat sifatnya trait Inv[-X]? Karena parameter parameternya contravarian X, Inv[X] <: Inv[Y]jika dan hanya jika Y <: X. Itu berarti bahwa kita dapat mengganti contoh di atas dengan yang benar-benar berfungsi:

trait Inv[-X]
def foo[X](implicit ev : (Inv[B] with Inv[String]) <:< Inv[X]) = {}

Itu karena ungkapan (Inv[U] with Inv[V]) <: Inv[X]itu benar, dengan asumsi yang sama di atas, tepat kapan Inv[U] <: Inv[X]atau Inv[V] <: Inv[X], dan dengan definisi contravariance, ini benar tepat ketika X <: Uatau X <: V.

Dimungkinkan untuk membuat hal-hal sedikit lebih dapat digunakan kembali dengan mendeklarasikan tipe parametrizable BOrString[X]dan menggunakannya sebagai berikut:

trait Inv[-X]
type BOrString[X] = (Inv[B] with Inv[String]) <:< Inv[X]
def foo[X](implicit ev : BOrString[X]) = {}

Scala sekarang akan mencoba untuk membangun tipe BOrString[X]untuk setiap Xyang foodipanggil dengan, dan tipe tersebut akan dibangun tepat ketika Xsubtipe dari salah satu Batau String. Itu bekerja, dan ada notasi steno. Sintaks di bawah ini setara (kecuali yang evsekarang harus direferensikan dalam tubuh metode sebagai implicitly[BOrString[X]]bukan hanya ev) dan digunakan BOrStringsebagai tipe konteks terikat :

def foo[X : BOrString] = {}

Apa yang benar-benar kita sukai adalah cara yang fleksibel untuk membuat konteks tipe terikat. Konteks tipe harus merupakan tipe parametrizable, dan kami ingin cara parametrizable untuk membuatnya. Kedengarannya seperti kita sedang mencoba menjilat fungsi pada tipe seperti kita menjilat fungsi pada nilai. Dengan kata lain, kami ingin sesuatu seperti yang berikut:

type Or[U,T][X] = (Inv[U] with Inv[T]) <:< Inv[X]

Itu tidak mungkin secara langsung di Scala, tetapi ada trik yang bisa kita gunakan untuk menjadi cukup dekat. Itu membawa kita ke definisi di Oratas:

trait Inv[-X]
type Or[U,T] = {
    type pf[X] = (Inv[U] with Inv[T]) <:< Inv[X]
}

Di sini kita menggunakan pengetikan struktural dan operator pound Scala untuk membuat tipe struktural Or[U,T]yang dijamin memiliki satu tipe internal. Ini adalah binatang aneh. Untuk memberikan beberapa konteks, fungsi tersebut def bar[X <: { type Y = Int }](x : X) = {}harus dipanggil dengan subkelas AnyRefyang memiliki tipe yang Yditentukan di dalamnya:

bar(new AnyRef{ type Y = Int }) // works!

Menggunakan operator pon memungkinkan kita untuk merujuk ke tipe dalam Or[B, String]#pf, dan menggunakan notasi infiks untuk operator tipe Or, kita sampai pada definisi asli kita tentang foo:

def foo[X : (B Or String)#pf] = {}

Kita dapat menggunakan fakta bahwa tipe fungsi bersifat contravarian dalam parameter tipe pertama mereka untuk menghindari mendefinisikan sifat Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
Josh
sumber
Bisakah ini menyelesaikan A|B <: A|B|Cmasalah? stackoverflow.com/questions/45255270/… Saya tidak tahu.
jhegedus
8

Ada juga retasan ini :

implicit val x: Int = 0
def foo(a: List[Int])(implicit ignore: Int) { }

implicit val y = ""
def foo(a: List[String])(implicit ignore: String) { }

foo(1::2::Nil)
foo("a"::"b"::Nil)

Lihat Mengatasi ambiguitas penghapusan tipe (Scala) .

michid
sumber
Lihat stackoverflow.com/questions/3422336/… . Sebenarnya ada peretasan yang lebih mudah: cukup tambahkan (implicit e: DummyImplicit)ke salah satu tanda tangan jenis.
Aaron Novstrup
7

Anda mungkin melihat MetaScala , yang memiliki sesuatu yang disebut OneOf. Saya mendapat kesan bahwa ini tidak bekerja dengan baik dengan matchpernyataan tetapi Anda dapat mensimulasikan pencocokan menggunakan fungsi tingkat tinggi. Sebagai contoh, lihat potongan ini , tetapi perhatikan bahwa bagian "pencocokan simulasi" dikomentari, mungkin karena belum berfungsi.

Sekarang untuk beberapa editorial: Saya tidak berpikir ada sesuatu yang mengerikan tentang mendefinisikan Either3, Either4, dll seperti yang Anda gambarkan. Ini pada dasarnya ganda untuk 22 jenis tuple standar yang dibangun untuk Scala. Pasti menyenangkan jika Scala memiliki tipe disjungtif bawaan, dan mungkin beberapa sintaks yang bagus untuk mereka {x, y, z}.

Tom Crockett
sumber
6

Saya berpikir bahwa tipe disjoint kelas pertama adalah supertipe tertutup, dengan subtipe alternatif, dan konversi implisit ke / dari tipe disjunction yang diinginkan ke subtipe alternatif ini.

Saya menganggap ini membahas komentar 33 - 36 dari solusi Miles Sabin, jadi tipe kelas pertama yang dapat digunakan di situs penggunaan, tetapi saya tidak mengujinya.

sealed trait IntOrString
case class IntOfIntOrString( v:Int ) extends IntOrString
case class StringOfIntOrString( v:String ) extends IntOrString
implicit def IntToIntOfIntOrString( v:Int ) = new IntOfIntOrString(v)
implicit def StringToStringOfIntOrString( v:String ) = new StringOfIntOrString(v)

object Int {
   def unapply( t : IntOrString ) : Option[Int] = t match {
      case v : IntOfIntOrString => Some( v.v )
      case _ => None
   }
}

object String {
   def unapply( t : IntOrString ) : Option[String] = t match {
      case v : StringOfIntOrString => Some( v.v )
      case _ => None
   }
}

def size( t : IntOrString ) = t match {
    case Int(i) => i
    case String(s) => s.length
}

scala> size("test")
res0: Int = 4
scala> size(2)
res1: Int = 2

Satu masalah adalah Scala tidak akan menggunakan dalam konteks kasus pencocokan, konversi implisit dari IntOfIntOrStringke Int(dan StringOfIntOrStringke String), jadi harus mendefinisikan extractors dan menggunakan case Int(i)alih-alih case i : Int.


TAMBAH: Saya merespons Miles Sabin di blognya sebagai berikut. Mungkin ada beberapa perbaikan di antaranya:

  1. Itu meluas ke lebih dari 2 jenis, tanpa kebisingan tambahan di situs penggunaan atau definisi.
  2. Argumen dikotakkan secara implisit, misalnya tidak perlu size(Left(2))atau size(Right("test")).
  3. Sintaks dari pencocokan pola secara implisit unboxed.
  4. Boxing dan unboxing dapat dioptimalkan jauh oleh hotspot JVM.
  5. Sintaksnya bisa menjadi yang diadopsi oleh tipe serikat kelas satu di masa depan, jadi migrasi mungkin bisa mulus? Mungkin untuk nama jenis gabungan, akan lebih baik digunakan Vdaripada Or, misalnya IntVString, ` Int |v| String`, ` Int or String`, atau favorit saya ` Int|String`?

UPDATE: Negasi logis dari disjungsi untuk pola di atas mengikuti, dan saya menambahkan alternatif (dan mungkin lebih bermanfaat) pola di blog Miles Sabin .

sealed trait `Int or String`
sealed trait `not an Int or String`
sealed trait `Int|String`[T,E]
case class `IntOf(Int|String)`( v:Int ) extends `Int|String`[Int,`Int or String`]
case class `StringOf(Int|String)`( v:String ) extends `Int|String`[String,`Int or String`]
case class `NotAn(Int|String)`[T]( v:T ) extends `Int|String`[T,`not an Int or String`]
implicit def `IntTo(IntOf(Int|String))`( v:Int ) = new `IntOf(Int|String)`(v)
implicit def `StringTo(StringOf(Int|String))`( v:String ) = new `StringOf(Int|String)`(v)
implicit def `AnyTo(NotAn(Int|String))`[T]( v:T ) = new `NotAn(Int|String)`[T](v)
def disjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `Int or String`) = x
def negationOfDisjunction[T,E](x: `Int|String`[T,E])(implicit ev: E =:= `not an Int or String`) = x

scala> disjunction(5)
res0: Int|String[Int,Int or String] = IntOf(Int|String)(5)

scala> disjunction("")
res1: Int|String[String,Int or String] = StringOf(Int|String)()

scala> disjunction(5.0)
error: could not find implicit value for parameter ev: =:=[not an Int or String,Int or String]
       disjunction(5.0)
                  ^

scala> negationOfDisjunction(5)
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction(5)
                            ^

scala> negationOfDisjunction("")
error: could not find implicit value for parameter ev: =:=[Int or String,not an Int or String]
       negationOfDisjunction("")
                            ^
scala> negationOfDisjunction(5.0)
res5: Int|String[Double,not an Int or String] = NotAn(Int|String)(5.0)

PEMBARUAN LAIN: Mengenai komentar 23 dan 35 dari solusi Mile Sabin , berikut adalah cara untuk mendeklarasikan jenis persatuan di situs penggunaan. Catat itu tidak dikotak setelah tingkat pertama, yaitu memiliki keuntungan yang dapat diperpanjang untuk sejumlah jenis dalam disjungsi , sedangkan Eitherkebutuhan bertinju tinju dan paradigma dalam komentar saya sebelumnya 41 tidak dapat diperpanjang. Dengan kata lain, a D[Int ∨ String]ditugaskan untuk (yaitu subtipe dari) a D[Int ∨ String ∨ Double].

type ¬[A] = (() => A) => A
type[T, U] = ¬[T] with ¬[U]
class D[-A](v: A) {
  def get[T](f: (() => T)) = v match {
    case x : ¬[T] => x(f)
  }
}
def size(t: D[IntString]) = t match {
  case x: D[¬[Int]] => x.get( () => 0 )
  case x: D[¬[String]] => x.get( () => "" )
  case x: D[¬[Double]] => x.get( () => 0.0 )
}
implicit def neg[A](x: A) = new D[¬[A]]( (f: (() => A)) => x )

scala> size(5)
res0: Any = 5

scala> size("")
error: type mismatch;
 found   : java.lang.String("")
 required: D[?[Int,String]]
       size("")
            ^

scala> size("hi" : D[¬[String]])
res2: Any = hi

scala> size(5.0 : D[¬[Double]])
error: type mismatch;
 found   : D[(() => Double) => Double]
 required: D[?[Int,String]]
       size(5.0 : D[?[Double]])
                ^

Rupanya kompiler Scala memiliki tiga bug.

  1. Ini tidak akan memilih fungsi implisit yang benar untuk semua jenis setelah jenis pertama dalam disjungsi tujuan.
  2. Itu tidak mengecualikan D[¬[Double]]kasing dari pertandingan.

3.

scala> class D[-A](v: A) {
  def get[T](f: (() => T))(implicit e: A <:< ¬[T]) = v match {
    case x : ¬[T] => x(f)
  }
}
error: contravariant type A occurs in covariant position in
       type <:<[A,(() => T) => T] of value e
         def get[T](f: (() => T))(implicit e: A <:< ?[T]) = v match {
                                           ^

Metode get tidak dibatasi dengan benar pada tipe input, karena kompiler tidak akan memungkinkan Adalam posisi kovarian. Orang mungkin berpendapat bahwa itu adalah bug karena yang kita inginkan hanyalah bukti, kita tidak pernah mengakses bukti dalam fungsinya. Dan saya membuat pilihan untuk tidak menguji case _dalam getmetode ini, jadi saya tidak perlu membuka Optionkotak matchdi dalam size().


05 Maret 2012: Pembaruan sebelumnya perlu ditingkatkan. Solusi Miles Sabin bekerja dengan benar dengan subtyping.

type ¬[A] = A => Nothing
type[T, U] = ¬[T] with ¬[U]
class Super
class Sub extends Super

scala> implicitly[(SuperString) <:< ¬[Super]]
res0: <:<[?[Super,String],(Super) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Sub]]
res2: <:<[?[Super,String],(Sub) => Nothing] = 

scala> implicitly[(SuperString) <:< ¬[Any]]
error: could not find implicit value for parameter
       e: <:<[?[Super,String],(Any) => Nothing]
       implicitly[(Super ? String) <:< ?[Any]]
                 ^

Proposal pembaruan saya sebelumnya (untuk tipe persatuan kelas dekat) mematahkan subtipe.

 scala> implicitly[D[¬[Sub]] <:< D[(SuperString)]]
error: could not find implicit value for parameter
       e: <:<[D[(() => Sub) => Sub],D[?[Super,String]]]
       implicitly[D[?[Sub]] <:< D[(Super ? String)]]
                 ^

Masalahnya adalah bahwa Adalam (() => A) => Amuncul di kedua kovarian (tipe kembali) dan contravarian (input fungsi, atau dalam hal ini nilai balik fungsi yang merupakan input fungsi) posisi, sehingga substitusi hanya dapat invarian.

Perhatikan bahwa A => Nothingdiperlukan hanya karena kita ingin Adi posisi contravariant, sehingga supertypes dari A tidak subtipe dari D[¬[A]]atau D[¬[A] with ¬[U]]( lihat juga ). Karena kita hanya memerlukan kontravarian ganda, kita dapat mencapai yang setara dengan solusi Miles bahkan jika kita dapat membuang ¬dan .

trait D[-A]

scala> implicitly[D[D[Super]] <:< D[D[Super] with D[String]]]
res0: <:<[D[D[Super]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Sub]] <:< D[D[Super] with D[String]]]
res1: <:<[D[D[Sub]],D[D[Super] with D[String]]] = 

scala> implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
error: could not find implicit value for parameter
       e: <:<[D[D[Any]],D[D[Super] with D[String]]]
       implicitly[D[D[Any]] <:< D[D[Super] with D[String]]]
                 ^

Jadi perbaikan lengkapnya adalah.

class D[-A] (v: A) {
  def get[T <: A] = v match {
    case x: T => x
  }
}

implicit def neg[A](x: A) = new D[D[A]]( new D[A](x) )

def size(t: D[D[Int] with D[String]]) = t match {
  case x: D[D[Int]] => x.get[D[Int]].get[Int]
  case x: D[D[String]] => x.get[D[String]].get[String]
  case x: D[D[Double]] => x.get[D[Double]].get[Double]
}

Catat 2 bug sebelumnya di Scala tetap, tetapi yang ke 3 dihindari karena Tsekarang dibatasi menjadi subtipe A.

Kami dapat mengkonfirmasi karya subtyping.

def size(t: D[D[Super] with D[String]]) = t match {
  case x: D[D[Super]] => x.get[D[Super]].get[Super]
  case x: D[D[String]] => x.get[D[String]].get[String]
}

scala> size( new Super )
res7: Any = Super@1272e52

scala> size( new Sub )
res8: Any = Sub@1d941d7

Saya telah berpikir bahwa jenis persimpangan kelas sangat penting, baik untuk alasan Ceylon memiliki mereka , dan karena bukannya subsuming untuk Anyyang berarti pembukaan kemasan dengan matchpada jenis diharapkan dapat menghasilkan runtime error, yang unboxing dari ( koleksi heterogen yang mengandung a) disjungsi dapat diketikkan jenisnya (Scala harus memperbaiki bug yang saya catat). Serikat pekerja yang lebih mudah daripada kompleksitas menggunakan eksperimental HList dari metascala untuk koleksi heterogen.

Shelby Moore III
sumber
Item # 3 di atas bukanlah bug di kompiler Scala . Catatan Saya awalnya tidak menomorinya sebagai bug, lalu dengan sembarangan melakukan edit hari ini dan melakukannya (lupa alasan asli saya untuk tidak menyatakannya adalah bug). Saya tidak mengedit posting lagi, karena saya berada di batas 7 suntingan.
Shelby Moore III
Bug # 1 di atas dapat dihindari dengan formulasi sizefungsi yang berbeda .
Shelby Moore III
Item # 2 bukanlah bug. Scala tidak dapat sepenuhnya mengekspresikan tipe serikat . Dokumen yang ditautkan menyediakan versi kode yang lain, sehingga sizetidak lagi menerima D[Any]sebagai masukan.
Shelby Moore III
Saya tidak mendapatkan jawaban ini, apakah ini juga jawaban untuk pertanyaan ini: stackoverflow.com/questions/45255270/…
jhegedus
5

Ada cara lain yang sedikit lebih mudah dipahami jika Anda tidak grok Curry-Howard:

type v[A,B] = Either[Option[A], Option[B]]

private def L[A,B](a: A): v[A,B] = Left(Some(a))
private def R[A,B](b: B): v[A,B] = Right(Some(b))  
// TODO: for more use scala macro to generate this for up to 22 types?
implicit def a2[A,B](a: A): v[A,B] = L(a)
implicit def b2[A,B](b: B): v[A,B] = R(b)
implicit def a3[A,B,C](a: A): v[v[A,B],C] = L(a2(a))
implicit def b3[A,B,C](b: B): v[v[A,B],C] = L(b2(b))
implicit def a4[A,B,C,D](a: A): v[v[v[A,B],C],D] = L(a3(a))
implicit def b4[A,B,C,D](b: B): v[v[v[A,B],C],D] = L(b3(b))    
implicit def a5[A,B,C,D,E](a: A): v[v[v[v[A,B],C],D],E] = L(a4(a))
implicit def b5[A,B,C,D,E](b: B): v[v[v[v[A,B],C],D],E] = L(b4(b))

type JsonPrimtives = (String v Int v Double)
type ValidJsonPrimitive[A] = A => JsonPrimtives

def test[A : ValidJsonPrimitive](x: A): A = x 

test("hi")
test(9)
// test(true)   // does not compile

Saya menggunakan teknik serupa di dijon

pathikrit
sumber
Bisakah ini bekerja dengan subtyping? Perasaan saya: tidak, tapi saya mungkin salah. stackoverflow.com/questions/45255270/…
jhegedus
1

Yah, itu semua sangat pintar, tapi saya cukup yakin Anda sudah tahu bahwa jawaban atas pertanyaan utama Anda adalah berbagai variasi "Tidak". Scala menangani kelebihan beban secara berbeda dan, harus diakui, agak kurang elegan dari yang Anda gambarkan. Beberapa di antaranya karena interoperabilitas Java, beberapa di antaranya adalah karena tidak ingin menekan kasus bermata jenis algoritma menyimpulkan, dan beberapa di antaranya karena itu tidak menjadi Haskell.

Dave Griffith
sumber
5
Sementara saya telah menggunakan Scala untuk sementara waktu, saya tidak sepengetahuan atau secerdas yang Anda pikirkan. Dalam contoh ini, saya bisa melihat bagaimana perpustakaan dapat memberikan solusinya. Masuk akal untuk kemudian bertanya-tanya apakah perpustakaan seperti itu ada (atau beberapa alternatif).
Aaron Novstrup
1

Menambah jawaban yang sudah bagus di sini. Berikut adalah intisari yang dibangun di atas tipe serikat Miles Sabin (dan ide-ide Josh) tetapi juga membuat mereka didefinisikan secara rekursif, sehingga Anda dapat memiliki> 2 jenis di serikat ( def foo[A : UNil Or Int Or String Or List[String])

https://gist.github.com/aishfenton/2bb3bfa12e0321acfc904a71dda9bfbb

NB: Saya harus menambahkan bahwa setelah bermain-main dengan di atas untuk sebuah proyek, saya akhirnya kembali ke tipe-biasa-lama-sum (yaitu sifat disegel dengan subclass). Jenis union Miles Sabin sangat bagus untuk membatasi parameter type, tetapi jika Anda perlu mengembalikan tipe union maka itu tidak menawarkan banyak.

Aish
sumber
Bisakah ini menyelesaikan A|C <: A|B|Cmasalah subtyping? stackoverflow.com/questions/45255270/... Perasaan saya TIDAK karena itu berarti bahwa A or Citu harus menjadi subtipe dari (A or B) or Ctetapi itu tidak mengandung tipe A or Csehingga tidak ada harapan dalam membuat A or Csubtipe A or B or Cdengan pengkodean ini setidaknya .. . Bagaimana menurut anda ?
jhegedus
0

Dari dokumen , dengan tambahan sealed:

sealed class Expr
case class Var   (x: String)          extends Expr
case class Apply (f: Expr, e: Expr)   extends Expr
case class Lambda(x: String, e: Expr) extends Expr

Mengenai sealedbagian:

Dimungkinkan untuk mendefinisikan kelas kasus lebih lanjut yang memperluas tipe Expr di bagian lain dari program (...). Bentuk ekstensibilitas ini dapat dikecualikan dengan mendeklarasikan kelas dasar Expr yang disegel; dalam hal ini, semua kelas yang secara langsung memperluas Expr harus dalam file sumber yang sama dengan Expr.

Elazar
sumber