Wie definiere ich "Typ Disjunktion" (Unionstypen)?

181

Eine Möglichkeit , dass hat vorgeschlagen, mit Doppel Definitionen überladener Methoden zu behandeln ist zu ersetzen durch Musterabgleich Überlastung:

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

Dieser Ansatz erfordert, dass wir die statische Typprüfung der Argumente an übergeben foo. Es wäre viel schöner, schreiben zu können

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

Ich kann nah dran sein Either, aber es wird schnell hässlich mit mehr als zwei Typen:

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

Es sieht aus wie ein General (elegant, effizient) Lösung wäre die Definition erfordern Either3, Either4.... Kennt jemand eine alternative Lösung , die das gleiche Ziel zu erreichen? Meines Wissens hat Scala keine eingebaute "Typdisjunktion". Lauern die oben definierten impliziten Konvertierungen irgendwo in der Standardbibliothek, sodass ich sie einfach importieren kann?

Aaron Novstrup
quelle

Antworten:

142

Nun, im speziellen Fall von Any*funktioniert dieser Trick unten nicht, da er keine gemischten Typen akzeptiert. Da gemischte Typen jedoch auch bei Überladung nicht funktionieren würden, ist dies möglicherweise das, was Sie möchten.

Deklarieren Sie zunächst eine Klasse mit den Typen, die Sie wie folgt akzeptieren möchten:

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

Als nächstes deklarieren Sie foowie folgt:

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

Und das ist es. Sie können foo(5)oder anrufen foo("abc"), und es wird funktionieren, aber versuchen foo(true)Sie es und es wird fehlschlagen. Dies kann durch den Clientcode umgangen werden, indem eine Klasse erstellt wird StringOrInt[Boolean], es sei denn, wie unten von Randall angegeben .StringOrIntsealed

Es funktioniert, weil T: StringOrIntes einen impliziten Parameter vom Typ StringOrInt[T]gibt und weil Scala in Begleitobjekten eines Typs nachschaut, um festzustellen, ob es implizite Elemente gibt, damit Code, der nach diesem Typ fragt, funktioniert.

Daniel C. Sobral
quelle
14
Wenn dies der Fall class StringOrInt[T]ist sealed, wird das von Ihnen erwähnte "Leck" ("Natürlich kann dies durch das Erstellen eines StringOrInt[Boolean]" durch den Client-Code umgangen werden ) verstopft, zumindest wenn es StringOrIntsich in einer eigenen Datei befindet. Dann müssen die Zeugenobjekte in derselben Quelle wie definiert werden StringOrInt.
Randall Schulz
3
Ich habe versucht, diese Lösung etwas zu verallgemeinern (als Antwort unten angegeben). Der Hauptnachteil gegenüber dem EitherAnsatz scheint zu sein, dass wir viel Compiler-Unterstützung für die Überprüfung der Übereinstimmung verlieren.
Aaron Novstrup
Guter Trick! Selbst mit der versiegelten Klasse können Sie sie im Clientcode umgehen, indem Sie entweder einen impliziten Wert b = new StringOrInt [Boolean] im Gültigkeitsbereich mit foo definieren oder explizit foo (2.9) (new StringOrInt [Double]) aufrufen. Ich denke, Sie müssen die Klasse auch abstrakt machen.
Paolo Falabella
2
Ja; es wäre wahrscheinlich besser zu verwendentrait StringOrInt ...
Mechanische Schnecke
7
Ps, wenn Sie Untertypen unterstützen möchten, ändern Sie einfach StringOrInt[T]zu StringOrInt[-T](siehe stackoverflow.com/questions/24387701/… )
Eran Medan
178

Miles Sabin beschreibt in seinem kürzlich veröffentlichten Blog-Beitrag Unboxed Union Types in Scala über den Curry-Howard-Isomorphismus einen sehr guten Weg, um Gewerkschaftstypen zu erhalten :

Er definiert zunächst die Negation von Typen als

type ¬[A] = A => Nothing

Mit dem Gesetz von De Morgan kann er Gewerkschaftstypen definieren

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

Mit den folgenden Hilfskonstrukten

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

Sie können Unionstypen wie folgt schreiben:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
quelle
13
Das ist eines der großartigsten Dinge, die ich je gesehen habe.
Submonoid
18
Hier ist meine erweiterte Implementierung der Idee von Miles: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - mit Beispielen: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ Test /…
Connor Doyle
6
Der obige Kommentar sollte eine Antwort für sich sein. Es ist nur eine Implementierung von Miles 'Idee, aber gut verpackt in einem Paket auf Maven Central und ohne all diese Unicode-Symbole, die irgendwo (?) Ein Problem für etwas in einem Build-Prozess darstellen könnten.
Jim Pivarski
2
Dieser lustige Charakter ist boolesche Negation .
Michid
1
Anfangs kam mir die Idee viel zu verworren vor. Als ich fast jeden in diesem Thread erwähnten Link las, war ich von der Idee und der Schönheit seiner Implementierung fasziniert :-) ... aber ich habe immer noch das Gefühl, dass dies etwas Verworrenes ist ... jetzt nur, weil es noch nicht direkt verfügbar ist weg von Scala. Wie Miles sagt: "Jetzt müssen wir nur noch Martin und Adriaan belästigen, um es direkt zugänglich zu machen."
Richard Gomes
44

Dotty , ein neuer experimenteller Scala-Compiler, unterstützt Unionstypen (geschrieben A | B), sodass Sie genau das tun können, was Sie wollten:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Samuel Gruetter
quelle
1
Einer dieser Tage.
Michael Ahlers
5
Dotty wird übrigens die neue Scala 3 sein (es wurde vor einigen Monaten angekündigt).
6infinity8
1
und wird irgendwo Ende 2020 verfügbar sein
JulienD
31

Hier ist die Rex Kerr-Methode zum Codieren von Vereinigungstypen. Geradlinig und einfach!

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

Quelle: Kommentar Nr. 27 unter diesem ausgezeichneten Blog-Beitrag von Miles Sabin, der eine weitere Möglichkeit zum Codieren von Unionstypen in Scala bietet.

fehlender Faktor
quelle
6
Leider kann diese Kodierung besiegt werden: scala> f(9.2: AnyVal)Übergibt den Typechecker.
Kipton Barros
@ Kipton: Das ist traurig. Leidet auch die Kodierung von Miles Sabin unter diesem Problem?
fehlender Faktor
9
Es gibt eine etwas einfachere Version des Miles-Codes. Da er tatsächlich die umgekehrte Implikation des kontravarianten Parameters der Funktion verwendet, kein striktes "Nicht", können Sie trait Contra[-A] {}anstelle aller Funktionen nichts verwenden. So bekommt man Sachen wie type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }gebraucht wie def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(ohne ausgefallenen Unicode).
Rex Kerr
Dies könnte das Vererbungsproblem von Unionstypen lösen? stackoverflow.com/questions/45255270/…
jhegedus
Hmm, ich habe es versucht, ich kann mit diesen Codierungen keine Rückgabetypen erstellen, daher scheint es nicht möglich zu sein, die Untertypisierung stackoverflow.com/questions/45255270/…
jhegedus
18

Es ist möglich, Daniels Lösung wie folgt zu verallgemeinern :

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

Die Hauptnachteile dieses Ansatzes sind

  • Wie Daniel betonte, werden Sammlungen / Varargs mit gemischten Typen nicht behandelt
  • Der Compiler gibt keine Warnung aus, wenn die Übereinstimmung nicht vollständig ist
  • Der Compiler gibt keinen Fehler aus, wenn die Übereinstimmung einen unmöglichen Fall enthält
  • Wie der EitherAnsatz würde weitere Verallgemeinerung erfordert analog zu definieren Or3, Or4usw. Merkmale. Das Definieren solcher Merkmale wäre natürlich viel einfacher als das Definieren der entsprechenden EitherKlassen.

Aktualisieren:

Mitch Blevins demonstriert einen sehr ähnlichen Ansatz und zeigt, wie man ihn auf mehr als zwei Typen verallgemeinert, indem man ihn als "Stottern oder" bezeichnet.

Aaron Novstrup
quelle
17

Ich bin auf eine relativ saubere Implementierung von n-ary Union-Typen gestoßen, indem ich den Begriff der Typlisten mit einer Vereinfachung von Miles Sabins Arbeit in diesem Bereich kombiniert habe , die jemand in einer anderen Antwort erwähnt.

Gegebener Typ, ¬[-A]der kontravariant istA , A <: Bkönnen wir per Definition schreiben ¬[B] <: ¬[A]und die Reihenfolge der Typen umkehren.

Gegebene Typen A, Bund X, möchten wir ausdrücken X <: A || X <: B. Wenn wir Kontravarianz anwenden, bekommen wir ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Dies kann wiederum so ausgedrückt werden, ¬[A] with ¬[B] <: ¬[X]dass einer von Aoder Bein Supertyp von Xoder sein mussX selbst sein muss (denken Sie an Funktionsargumente).

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
}

Ich habe einige Zeit damit verbracht, diese Idee mit einer Obergrenze für Elementtypen zu kombinieren, wie in TLists von harrah / up zu sehen , aber die Implementierung von Mapmit Typgrenzen hat sich bisher als schwierig erwiesen.

J Cracknell
quelle
1
Das ist genial, danke! Ich habe die früheren Ansätze ausprobiert, hatte aber immer wieder Probleme, diese mit generischen Typen als Teil der Gewerkschaft zu verwenden. Dies war die einzige Implementierung, mit der ich mit generischen Typen arbeiten konnte.
Samer Adra
Leider, aber wahrscheinlich zu erwarten, funktioniert es nicht, wenn ich versuche, eine Scala-Methode zu verwenden, die einen Union-Typ aus Java-Code aufnimmt. Fehler: (40, 29) java: Die Methode setValue in der Klasse Config kann nicht auf bestimmte Typen angewendet werden. Erforderlich: X, scala.Predef. $ less $ Doppelpunkt $ less <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> gefunden: java.lang.String Grund: Kann nicht schließen Typvariable (n) X (tatsächliche und formale Argumentlisten unterscheiden sich in der Länge)
Samer Adra
Einige Details dieser Implementierung sind noch nicht ganz klar. Zum Beispiel definierte der ursprüngliche Artikel die Negation als "Typ ¬ [A] = A => Nichts", aber in dieser Version, wenn nur "versiegeltes Merkmal ¬ [-A]" vorliegt und das Merkmal nirgendwo erweitert wird. Wie funktioniert das?
Samer Adra
@Samer Adra Es würde so oder so funktionieren, der Artikel wird Function1als vorhandener kontravarianter Typ verwendet. Sie benötigen keine Implementierung, Sie benötigen lediglich einen Konformitätsnachweis ( <:<).
J Cracknell
Irgendeine Idee, wie man einen Konstruktor hat, der einen Unionstyp akzeptiert?
Samer Adra
12

Eine Typklassenlösung ist wahrscheinlich der schönste Weg, um implizite Ergebnisse zu erzielen. Dies ähnelt dem im Buch Odersky / Spoon / Venners erwähnten monoiden Ansatz:

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)

Wenn Sie dies dann in der REPL ausführen:

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
quelle
Ich könnte mich irren, aber ich glaube nicht, dass das OP danach gesucht hat. OP fragte nach einem Datentyp, der eine disjunkte Vereinigung von Typen darstellen könnte, und führte dann zur Laufzeit eine Fallanalyse durch, um festzustellen , wie sich der tatsächliche Typ herausstellte. Typklassen lösen dieses Problem nicht, da es sich um ein Konstrukt zur reinen Kompilierungszeit handelt.
Tom Crockett
4
Die eigentliche Frage war, wie unterschiedliche Verhaltensweisen für unterschiedliche Typen verfügbar gemacht werden können, jedoch ohne Überlastung. Ohne Kenntnis der Typklassen (und möglicherweise einiger Kenntnisse in C / C ++) scheint ein Unionstyp die einzige Lösung zu sein. Scalas bereits existierender EitherTyp verstärkt diesen Glauben. Die Verwendung von Typklassen über Scalas Implikits ist eine bessere Lösung für das zugrunde liegende Problem, aber es ist ein relativ neues Konzept und noch nicht allgemein bekannt, weshalb das OP nicht einmal wusste, sie als mögliche Alternative zu einem Unionstyp zu betrachten.
Kevin Wright
funktioniert das mit subtyping? stackoverflow.com/questions/45255270/…
jhegedus
10

Wir möchten einen Typoperator Or[U,V], mit dem Typparameter Xso eingeschränkt werden können, dass entweder X <: Uoder X <: V. Hier ist eine Definition, die so nah wie möglich kommt:

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

So wird es verwendet:

// 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!

Dies verwendet einige Scala-Tricks. Das wichtigste ist die Verwendung verallgemeinerter Typbeschränkungen . Bei gegebenen Typen Uund Vstellt der Scala-Compiler eine Klasse namens U <:< V(und ein implizites Objekt dieser Klasse) bereit , wenn und nur wenn der Scala-Compiler nachweisen kann, dass Ues sich um einen Subtyp von handelt V. Hier ist ein einfacheres Beispiel mit allgemeinen Typeinschränkungen, das in einigen Fällen funktioniert:

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

Dieses Beispiel funktioniert, wenn Xeine Instanz der Klasse Ba Stringoder einen Typ hat, der weder ein Supertyp noch ein Subtyp von Boder ist String. In den ersten beiden Fällen ist es durch die Definition des withSchlüsselworts wahr, dass (B with String) <: Bund (B with String) <: String, so dass Scala ein implizites Objekt bereitstellt, das übergeben wird als ev: Der Scala-Compiler akzeptiert foo[B]und korrekt foo[String].

Im letzten Fall verlasse ich mich auf die Tatsache, dass wenn U with V <: X, dann U <: Xoder V <: X. Es scheint intuitiv wahr zu sein, und ich gehe einfach davon aus. Aus dieser Annahme geht hervor, warum dieses einfache Beispiel fehlschlägt, wenn Xein Supertyp oder Subtyp von entweder Boder String: im obigen Beispiel foo[A]falsch akzeptiert und foo[C]falsch abgelehnt wird. Auch hier , was wir wollen , ist eine Art von Typ - Expression auf den Variablen U, Vund Xdas stimmt genau , wann X <: UoderX <: V .

Scalas Begriff der Kontravarianz kann hier helfen. Erinnerst trait Inv[-X]du dich an das Merkmal ? Weil es in seinem Typparameter genau dann kontravariant ist X, Inv[X] <: Inv[Y]wenn Y <: X. Das bedeutet, dass wir das obige Beispiel durch eines ersetzen können, das tatsächlich funktioniert:

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

Das liegt daran, dass der Ausdruck (Inv[U] with Inv[V]) <: Inv[X]nach der gleichen Annahme oben genau wann Inv[U] <: Inv[X]oder Inv[V] <: Inv[X]und nach der Definition der Kontravarianz genau dann wahr ist, wenn X <: Uoder X <: V.

Es ist möglich, die Wiederverwendbarkeit zu verbessern, indem ein parametrierbarer Typ deklariert BOrString[X]und wie folgt verwendet wird:

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

Scala versucht nun , den Typen zu konstruieren , BOrString[X]für jeden X, der foomit genannt wird, und die Art genau gebaut werden , wenn Xein Subtyp von entweder Boder String. Das funktioniert und es gibt eine Kurzschreibweise. Die folgende Syntax ist äquivalent (mit der Ausnahme, dass evjetzt im Methodenkörper implicitly[BOrString[X]]als einfach darauf verwiesen werden muss ev) und wird BOrStringals Typkontext gebunden verwendet :

def foo[X : BOrString] = {}

Was wir wirklich möchten, ist eine flexible Möglichkeit, einen Typkontext gebunden zu erstellen. Ein Typkontext muss ein parametrisierbarer Typ sein, und wir möchten einen parametrisierbaren Weg, um einen zu erstellen. Das klingt so, als würden wir versuchen, Funktionen für Typen zu curry, genauso wie wir Funktionen für Werte curryen. Mit anderen Worten, wir möchten so etwas wie das Folgende:

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

Das ist in Scala nicht direkt möglich , aber es gibt einen Trick, mit dem wir uns ziemlich nahe kommen können. Das bringt uns zur Orobigen Definition :

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

Hier verwenden wir die strukturelle Typisierung und den Pfund-Operator von Scala , um einen strukturellen Typ zu erstellen Or[U,T], der garantiert einen internen Typ hat. Das ist ein seltsames Tier. Um einen Kontext zu geben, def bar[X <: { type Y = Int }](x : X) = {}muss die Funktion mit Unterklassen aufgerufen werden AnyRef, in denen ein Typ Ydefiniert ist:

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

Wenn wir den Pfund-Operator verwenden, können wir uns auf den inneren Typ beziehen Or[B, String]#pf, und wennOr wir die Infix-Notation für den Typ-Operator verwenden , gelangen wir zu unserer ursprünglichen Definition von foo:

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

Wir können die Tatsache nutzen, dass Funktionstypen in ihrem ersten Typparameter kontravariant sind, um zu vermeiden, dass das Merkmal definiert wird Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
Josh
quelle
Kann dies das A|B <: A|B|CProblem lösen ? stackoverflow.com/questions/45255270/… Ich kann es nicht sagen.
Jhegedus
7

Sie können sich MetaScala ansehen , das so etwas wie heißt OneOf. Ich habe den Eindruck, dass dies mit matchAnweisungen nicht gut funktioniert, Sie aber den Abgleich mit Funktionen höherer Ordnung simulieren können. Schauen Sie sich diesen Ausschnitt an zum Beispiel an, aber beachten Sie, dass der Teil "Simuliertes Matching" auskommentiert ist, vielleicht weil es noch nicht ganz funktioniert.

Nun zu einer redaktionellen Bearbeitung: Ich glaube nicht, dass die Definition von Entweder3, Entweder4 usw., wie Sie sie beschreiben, etwas Ungeheuerliches ist. Dies ist im Wesentlichen doppelt so hoch wie die in Scala integrierten 22 Standardtupeltypen. Es wäre sicherlich schön, wenn Scala disjunktive Typen eingebaut hätte und vielleicht eine nette Syntax für sie wie {x, y, z}.

Tom Crockett
quelle
6

Ich denke, dass der disjunkte Typ der ersten Klasse ein versiegelter Supertyp mit den alternativen Subtypen und impliziten Konvertierungen zu / von den gewünschten Typen der Disjunktion zu diesen alternativen Subtypen ist.

Ich gehe davon aus, dass dies die Kommentare 33 - 36 der Lösung von Miles Sabin anspricht , also den erstklassigen Typ, der am Verwendungsort verwendet werden kann, aber ich habe ihn nicht getestet.

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

Ein Problem ist, dass Scala bei übereinstimmendem Kontext keine implizite Konvertierung von IntOfIntOrStringnach Int(und StringOfIntOrStringnach String) verwendet. Daher müssen Extraktoren definiert und case Int(i)stattdessen verwendet werden case i : Int.


ADD: Ich habe Miles Sabin in seinem Blog wie folgt geantwortet. Vielleicht gibt es einige Verbesserungen gegenüber Entweder:

  1. Es erstreckt sich auf mehr als zwei Typen, ohne zusätzliches Rauschen an der Verwendungs- oder Definitionsstelle.
  2. Argumente sind implizit eingerahmt, zB brauchen size(Left(2))oder nicht size(Right("test")).
  3. Die Syntax des Mustervergleichs ist implizit entpackt.
  4. Das Boxen und Entpacken kann durch den JVM-Hotspot optimiert werden.
  5. Die Syntax könnte die eines zukünftigen erstklassigen Unionstyps sein, sodass die Migration möglicherweise nahtlos sein könnte. Vielleicht wäre es für den Namen Vdes Unionstyps besser, anstelle von Orz. B. IntVString` Int |v| String`, ` Int or String` oder meinem Favoriten ` Int|String` zu verwenden?

UPDATE: Es folgt eine logische Negation der Disjunktion für das obige Muster, und ich habe ein alternatives (und wahrscheinlich nützlicheres) Muster in Miles Sabins Blog hinzugefügt .

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)

EIN ANDERES UPDATE: In Bezug auf die Kommentare 23 und 35 der Lösung von Mile Sabin gibt es hier eine Möglichkeit, einen Vereinigungstyp am Verwendungsort zu deklarieren. Beachten Sie, dass es nach der ersten Ebene nicht mehr verpackt ist, dh es hat den Vorteil, dass es auf eine beliebige Anzahl von Typen in der Disjunktion erweiterbar ist , während Eitherverschachteltes Boxen erforderlich ist und das Paradigma in meinem vorherigen Kommentar 41 nicht erweiterbar war. Mit anderen Worten, a kann a D[Int ∨ String]zugewiesen werden (dh ist ein Subtyp von) 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]])
                ^

Anscheinend hat der Scala-Compiler drei Fehler.

  1. Nach dem ersten Typ in der Zieldisjunktion wird für keinen Typ die richtige implizite Funktion ausgewählt.
  2. Der Fall wird nicht D[¬[Double]]vom Spiel ausgeschlossen.

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 {
                                           ^

Die get-Methode ist für den Eingabetyp nicht richtig eingeschränkt, da der Compiler Adie kovariante Position nicht zulässt . Man könnte argumentieren, dass dies ein Fehler ist, da wir nur Beweise wollen und niemals auf die Beweise in der Funktion zugreifen. Und ich habe die Entscheidung getroffen, nicht case _in der getMethode zu testen , damit ich keine Optionin der matchIn entpacken muss size().


05. März 2012: Das vorherige Update muss verbessert werden. Die Lösung von Miles Sabin funktionierte korrekt mit Subtypisierung.

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]]
                 ^

Der Vorschlag meines vorherigen Updates (für einen nahezu erstklassigen Gewerkschaftstyp) hat die Untertypisierung unterbrochen.

 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)]]
                 ^

Das Problem ist, dass Ain(() => A) => A sowohl in der Kovariante (Rückgabetyp) als auch in der Kontravariante (Funktionseingabe oder in diesem Fall ein Rückgabewert der Funktion, die eine Funktionseingabe ist) auftritt, sodass Substitutionen nur invariant sein können.

Beachten Sie, dass A => Nothingdies nur notwendig ist, weil wir Ain der kontravarianten Position wollen, damit Supertypen von A weder Subtypen von D[¬[A]]noch sind D[¬[A] with ¬[U]]( siehe auch ). Da wir nur eine doppelte Kontravarianz benötigen, können wir das Äquivalent zur Miles-Lösung erreichen, selbst wenn wir das ¬und verwerfen können .

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]]]
                 ^

Das komplette Update ist also.

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

Beachten Sie, dass die vorherigen 2 Fehler in Scala bestehen bleiben, der dritte jedoch vermieden wird, da er Tnun auf den Subtyp beschränkt ist A.

Wir können die Untertypisierung bestätigen.

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

Ich habe nur gedacht , dass erstklassige Schnittarten sind sehr wichtig, sowohl für die Gründe Ceylon sie hat , und weil statt subsumieren zu , Anywelche Mittel mit einem Unboxing matchauf erwarteten Typen können einen Laufzeitfehler erzeugen, die Unboxing eines ( heterogene Sammlung enthalten a) Die Disjunktion kann typgeprüft werden (Scala muss die von mir festgestellten Fehler beheben). Gewerkschaften sind einfacher als die Komplexität der Verwendung der experimentellen HList von Metascala für heterogene Sammlungen.

Shelby Moore III
quelle
Das obige Element Nr. 3 ist kein Fehler im Scala-Compiler . Hinweis: Ich hatte es ursprünglich nicht als Fehler nummeriert, dann heute unachtsam eine Bearbeitung vorgenommen und dies getan (wobei ich meinen ursprünglichen Grund vergaß, nicht anzugeben, dass es sich um einen Fehler handelte). Ich habe den Beitrag nicht erneut bearbeitet, da ich mich an der 7-Bearbeitungsgrenze befinde.
Shelby Moore III
Der obige Fehler Nr. 1 kann mit einer anderen Formulierung der sizeFunktion vermieden werden .
Shelby Moore III
Das Element Nr. 2 ist kein Fehler. Scala kann einen Unionstyp nicht vollständig ausdrücken . Das verknüpfte Dokument enthält eine andere Version des Codes, sodass diese sizenicht mehr D[Any]als Eingabe akzeptiert wird .
Shelby Moore III
Ich verstehe diese Antwort nicht ganz, ist dies auch eine Antwort auf diese Frage: stackoverflow.com/questions/45255270/…
jhegedus
5

Es gibt einen anderen Weg, der etwas einfacher zu verstehen ist, wenn Sie Curry-Howard nicht groken:

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

Ich benutze ähnliche Techniken in Dijon

Pathikrit
quelle
Kann das mit Subtyping funktionieren? Mein Bauchgefühl: Nein, aber ich könnte mich irren. stackoverflow.com/questions/45255270/…
jhegedus
1

Nun, das ist alles sehr klug, aber ich bin mir ziemlich sicher, dass Sie bereits wissen, dass die Antworten auf Ihre Leitfragen verschiedene Arten von "Nein" sind. Scala geht anders mit Überladung um und muss zugegebenermaßen etwas weniger elegant sein, als Sie beschreiben. Ein Teil davon ist auf die Java-Interoperabilität zurückzuführen, ein anderer Teil darauf, dass nicht kantige Fälle des Typ-Inferenz-Algorithmus getroffen werden sollen, und ein anderer Teil darauf, dass es einfach nicht Haskell ist.

Dave Griffith
quelle
5
Während ich Scala schon eine Weile benutze, bin ich weder so sachkundig noch so schlau, wie Sie zu denken scheinen. In diesem Beispiel kann ich sehen, wie eine Bibliothek die Lösung bereitstellen kann. Es ist sinnvoll, sich dann zu fragen, ob eine solche Bibliothek existiert (oder eine Alternative).
Aaron Novstrup
1

Hinzu kommen die bereits tollen Antworten hier. Hier ist ein Kern, der auf Miles Sabin-Unionstypen (und Joshs Ideen) aufbaut, sie aber auch rekursiv definiert, sodass Sie> 2 Typen in der Union haben können ( def foo[A : UNil Or Int Or String Or List[String])

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

NB: Ich sollte hinzufügen, dass ich, nachdem ich für ein Projekt mit den oben genannten Dingen herumgespielt habe, zu einfachen alten Summentypen zurückgekehrt bin (dh zu einem versiegelten Merkmal mit Unterklassen). Miles Sabin-Unionstypen eignen sich hervorragend zum Einschränken des Typparameters. Wenn Sie jedoch einen Unionstyp zurückgeben müssen, bietet er nicht viel.

Aish
quelle
Kann dies das A|C <: A|B|CSubtypisierungsproblem lösen? stackoverflow.com/questions/45255270/… Mein Bauchgefühl NEIN, denn dann würde es bedeuten, A or Cdass dies der Subtyp von sein müsste, der (A or B) or Caber nicht den Typ enthält, A or Cso dass es keine Hoffnung gibt , zumindest mit dieser Codierung A or Ceinen Subtyp A or B or Cdaraus zu machen. . Was denken Sie ?
Jhegedus
0

Aus den Dokumenten mit dem Zusatz von 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

Zum sealedTeil:

Es ist möglich, weitere Fallklassen zu definieren, die den Typ Expr in anderen Teilen des Programms erweitern (...). Diese Form der Erweiterbarkeit kann ausgeschlossen werden, indem die Basisklasse Expr als versiegelt deklariert wird. In diesem Fall müssen sich alle Klassen, die Expr direkt erweitern, in derselben Quelldatei wie Expr befinden.

Elazar
quelle