Come definire "type disjunction" (tipi di unione)?


181

Un modo che è stato suggerito per trattare le doppie definizioni dei metodi sovraccarichi è quello di sostituire il sovraccarico con la corrispondenza dei modelli:

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

Questo approccio richiede di rinunciare al controllo del tipo statico sugli argomenti foo. Sarebbe molto più bello poter scrivere

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

Posso avvicinarmi Either, ma diventa brutto veloce con più di due tipi:

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

Sembra una soluzione generale (elegante, efficiente) richiederebbe la definizione Either3, Either4, .... qualcuno sa di una soluzione alternativa per raggiungere lo stesso fine? Per quanto ne sappia, Scala non ha una "disgiunzione di tipo" incorporata. Inoltre, le conversioni implicite definite sopra si nascondono nella libreria standard da qualche parte in modo da poterle semplicemente importare?

Risposte:


142

Bene, nel caso specifico di Any*, questo trucco di seguito non funzionerà, in quanto non accetterà tipi misti. Tuttavia, poiché i tipi misti non funzionerebbero neanche con il sovraccarico, questo potrebbe essere quello che vuoi.

Prima di tutto, dichiara una classe con i tipi che desideri accettare come di seguito:

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

Quindi, dichiarare in fooquesto modo:

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

E questo è tutto. Puoi chiamare foo(5)o foo("abc"), e funzionerà, ma prova foo(true)e fallirà. Questo potrebbe essere evitato dal codice client creando un StringOrInt[Boolean], a meno che, come indicato da Randall di seguito, si crei StringOrIntunsealed classe.

Funziona perché T: StringOrIntsignifica che c'è un parametro implicito di tipo StringOrInt[T], e perché Scala guarda all'interno degli oggetti companion di un tipo per vedere se ci sono impliciti lì per far funzionare il codice che richiede quel tipo.


14
Se class StringOrInt[T]viene creato sealed, la "perdita" a cui si fa riferimento ("Naturalmente, questo potrebbe essere evitato dal codice client creando un StringOrInt[Boolean]") viene inserito, almeno se StringOrIntrisiede in un file a sé stante. Quindi gli oggetti testimone devono essere definiti nello stesso formato di StringOrInt.
Randall Schulz,

3
Ho provato a generalizzare in qualche modo questa soluzione (pubblicata come risposta di seguito). Lo svantaggio principale rispetto Eitherall'approccio sembra essere la perdita del supporto del compilatore per il controllo della corrispondenza.
Aaron Novstrup,

bel trucco! Tuttavia, anche con la classe sigillata, è comunque possibile aggirarla nel codice client definendo una val implicita b = new StringOrInt [Boolean] nell'ambito con foo o chiamando esplicitamente foo (2.9) (new StringOrInt [Double]). Penso che anche tu debba rendere astratta la classe.
Paolo Falabella,

2
Sì; probabilmente sarebbe meglio usaretrait StringOrInt ...
Lumaca meccanica il

7
Ps se vuoi supportare i sottotipi, cambia semplicemente StringOrInt[T]in StringOrInt[-T](vedi stackoverflow.com/questions/24387701/… )
Eran Medan,

178

Miles Sabin descrive un modo molto carino per ottenere il tipo di unione nel suo recente post sul blog Unboxed tipo di unione a Scala tramite l'isomorfismo di Curry-Howard :

Definisce dapprima la negazione dei tipi come

type ¬[A] = A => Nothing

usando la legge di De Morgan questo gli permette di definire i tipi di unione

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

Con i seguenti costrutti ausiliari

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

puoi scrivere i tipi di unione come segue:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}

13
Questa è una delle cose più belle che abbia mai visto.
Submonoid


6
Il commento sopra dovrebbe essere una risposta a sé stante. È solo un'implementazione dell'idea di Miles, ma racchiusa in un pacchetto su Maven Central e senza tutti quei simboli unicode che potrebbero (?) Rappresentare un problema per qualcosa in un processo di compilazione da qualche parte.
Jim Pivarski,

2
Quel personaggio divertente è la negazione booleana .
michid

1
Inizialmente, l'idea mi sembrava troppo contorta. Leggendo quasi tutti i link citati in questo thread, sono stato catturato dall'idea e dalla bellezza della sua implementazione :-) ... ma mi sento ancora come questo è qualcosa di contorto ... ora solo perché non è ancora disponibile direttamente lontano da Scala. Come dice Miles: "Ora dobbiamo solo assillare Martin e Adriaan per renderlo direttamente accessibile."
Richard Gomes,

44

Dotty , un nuovo compilatore sperimentale di Scala, supporta i tipi di unione (scritti A | B), quindi puoi fare esattamente quello che volevi:

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

1
Uno di questi giorni.
Michael Ahlers,

5
A proposito, Dotty sarà la nuova scala 3 (è stata annunciata alcuni mesi fa).
6infinity8

1
e sarà disponibile da qualche parte alla fine del 2020
JulienD

31

Ecco il modo Rex Kerr per codificare i tipi di unione. Dritto e semplice!

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

Fonte: commento n. 27 sotto questo eccellente post sul blog di Miles Sabin che fornisce un altro modo per codificare i tipi di sindacato in Scala.


6
Sfortunatamente, questa codifica può essere sconfitta: scala> f(9.2: AnyVal)passa il typechecker.
Kipton Barros,

@Kipton: è triste. Anche la codifica di Miles Sabin soffre di questo problema?
missingfaktor

9
Esiste una versione leggermente più semplice del codice Miles; dal momento che sta effettivamente usando l'implicazione inversa del parametro controvariante della funzione, non un "no" rigoroso, è possibile utilizzare trait Contra[-A] {}al posto di tutte le funzioni per nulla. Quindi ottieni cose come type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }usate come def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(senza fantasia unicode).
Rex Kerr,

Questo potrebbe risolvere il problema dell'eredità dei tipi di unione? stackoverflow.com/questions/45255270/...
jhegedus

Hmm, ho provato, non posso creare tipi di ritorno con questo codifiche, quindi la sua non sembra essere possibile implementare sottotipizzazione stackoverflow.com/questions/45255270/...
jhegedus

18

È possibile generalizzare la soluzione di Daniel come segue:

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

Gli svantaggi principali di questo approccio sono

  • Come ha sottolineato Daniel, non gestisce collezioni / vararg con tipi misti
  • Il compilatore non emette un avviso se la corrispondenza non è esaustiva
  • Il compilatore non genera un errore se la corrispondenza include un caso impossibile
  • Come l' Eitherapproccio, inoltre generalizzazione richiederebbe la definizione analoga Or3, Or4ecc tratti. Naturalmente, definire tali tratti sarebbe molto più semplice che definire le Eitherclassi corrispondenti .

Aggiornare:

Mitch Blevins dimostra un approccio molto simile e mostra come generalizzarlo in più di due tipi, soprannominandolo "balbuzie".


18

Mi sono imbattuto in un'implementazione relativamente pulita di tipi di unione n-ary combinando la nozione di elenchi di tipi con una semplificazione del lavoro di Miles Sabin in questo settore , che qualcuno menziona in un'altra risposta.

Dato il tipo ¬[-A]che è contraddittorio A, per definizione dato A <: Bche possiamo scrivere ¬[B] <: ¬[A] , invertendo l'ordinamento dei tipi.

Dati tipi A, Be X, vogliamo esprimere X <: A || X <: B. Applicando la contraddizione, otteniamo ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Questo a sua volta può essere espresso come¬[A] with ¬[B] <: ¬[X] in quale uno Ao Bdeve essere un supertipo di Xo Xse stesso (pensare agli argomenti delle funzioni).

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
}

Ho trascorso un po 'di tempo a provare a combinare questa idea con un limite superiore per i tipi di membri, come si vede nella TLists di harrah / up , tuttavia l'implementazione di Mapcon limiti di tipo si è finora rivelata impegnativa.


1
Questo è geniale, grazie! Ho provato gli approcci precedenti ma continuavo ad avere problemi nell'usarlo con tipi generici come parte del sindacato. Questa è stata l'unica implementazione che ho potuto ottenere lavorando con tipi generici.
Samer Adra,

Purtroppo, ma probabilmente ci si aspetta, quando provo ad usare un metodo Scala che accetta un tipo di unione dal codice Java, non funziona. Errore: (40, 29) java: metodo setValue nella classe Config non può essere applicato a determinati tipi; richiesto: X, scala.Predef. $ meno $ due punti $ meno <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> trovato: java.lang.String motivo: impossibile inferire type-variabile (s) X (gli elenchi di argomenti effettivi e formali differiscono in lunghezza)
Samer Adra

Ancora non abbastanza chiaro su alcuni dettagli di questa implementazione. Ad esempio, l'articolo originale ha definito la negazione come "tipo ¬ [A] = A => Nothing" ma in questa versione se ha solo "tratto sigillato ¬ [-A]" e il tratto non è esteso da nessuna parte. Come funziona?
Samer Adra,

@Samer Adra Funzionerebbe in entrambi i modi, l'articolo usa Function1come un tipo di contraddittorio esistente. Non hai bisogno di un'implementazione, tutto ciò che serve è la prova di conformità ( <:<).
J Cracknell,

Qualche idea su come avere un costruttore che accetta un tipo di unione?
Samer Adra,

13

Una soluzione di classe di tipo è probabilmente il modo migliore per andare qui, usando impliciti. Questo è simile all'approccio monoide menzionato nel libro di 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)

Se si esegue quindi questo nel 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)

              ^

Potrei sbagliarmi, ma non penso che questo sia ciò che l'OP stava cercando. OP stava chiedendo informazioni su un tipo di dati che potesse rappresentare un'unione di tipi disgiunti, e quindi eseguire un'analisi dei casi su di esso in fase di esecuzione per vedere quale fosse il tipo effettivo. Le classi di tipi non risolveranno questo problema, poiché sono un costrutto in fase di compilazione.
Tom Crockett,

5
La vera domanda posta era come esporre comportamenti diversi per tipi diversi, ma senza sovraccarico. Senza la conoscenza delle classi di tipi (e forse una certa esposizione a C / C ++), un tipo di unione sembra essere l'unica soluzione. Il Eithertipo preesistente di Scala tende a rafforzare questa convinzione. L'uso delle classi di tipi tramite gli impliciti di Scala è una soluzione migliore al problema di fondo, ma è un concetto relativamente nuovo e ancora non ampiamente conosciuto, motivo per cui l'OP non sapeva nemmeno di considerarli come una possibile alternativa a un tipo di unione.
Kevin Wright,


10

Vorremmo un operatore di tipo Or[U,V]che può essere utilizzato per vincolare i parametri di un tipo Xin modo tale che X <: Uo X <: V. Ecco una definizione che si avvicina il più vicino possibile:

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

Ecco come viene utilizzato:

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

Questo utilizza alcuni trucchi di tipo Scala. Il principale è l'uso di vincoli di tipo generalizzati . Dati tipi Ue V, il compilatore Scala fornisce una classe chiamata U <:< V(e un oggetto implicito di quella classe) se e solo se il compilatore Scala può dimostrare che Uè un sottotipo di V. Ecco un esempio più semplice usando i vincoli di tipo generalizzati che funzionano in alcuni casi:

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

Questo esempio funziona quando Xun'istanza di classe B, a Stringo ha un tipo che non è né un supertipo né un sottotipo di Bo String. Nei primi due casi, è vero per la definizione della withparola chiave that (B with String) <: Be (B with String) <: String, quindi Scala fornirà un oggetto implicito che verrà passato come ev: il compilatore Scala accetterà correttamente foo[B]e foo[String].

Nell'ultimo caso, mi affido al fatto che if U with V <: X, then U <: Xor V <: X. Sembra intuitivamente vero e lo sto semplicemente assumendo. È chiaro da questo presupposto perché questo semplice esempio fallisce quando Xè un supertipo o un sottotipo di uno Bo due String: ad esempio, nell'esempio sopra, foo[A]viene accettato in modo errato e foo[C]viene rifiutato in modo errato. Anche in questo caso, quello che vogliamo è una sorta di tipo di espressione sulle variabili U, Ve Xche è vero esattamente quando X <: UoX <: V .

La nozione di scala di contraddizione di Scala può aiutare qui. Ricorda il tratto trait Inv[-X]? Perché è contraddittorio nel suo parametro di tipo X, Inv[X] <: Inv[Y]se e solo se Y <: X. Ciò significa che possiamo sostituire l'esempio sopra con uno che funzionerà effettivamente:

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

Questo perché l'espressione (Inv[U] with Inv[V]) <: Inv[X]è vera, con lo stesso presupposto sopra, esattamente quando Inv[U] <: Inv[X]o Inv[V] <: Inv[X], e con la definizione di contraddizione, questo è vero esattamente quando X <: Uo X <: V.

È possibile rendere le cose un po 'più riutilizzabili dichiarando un tipo parametrizzabile BOrString[X]e utilizzandolo come segue:

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

Scala ora tentare di costruire il tipo BOrString[X]per ogni Xche fooviene chiamato con, e il tipo verrà costruita con precisione quando Xè un sottotipo di uno Bo String. Funziona e c'è una notazione abbreviata. La sintassi seguente è equivalente (tranne che evora deve essere referenziata nel corpo del metodo implicitly[BOrString[X]]piuttosto che semplicemente ev) e utilizza BOrStringcome tipo contesto associato :

def foo[X : BOrString] = {}

Quello che vorremmo davvero è un modo flessibile per creare un tipo associato al contesto. Un contesto di tipo deve essere un tipo parametrizzabile e desideriamo un modo parametrizzabile per crearne uno. Sembra che stiamo provando a eseguire il curry delle funzioni sui tipi, proprio come facciamo a curry sulle funzioni dei valori. In altre parole, vorremmo qualcosa di simile al seguente:

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

Questo non è direttamente possibile in Scala, ma c'è un trucco che possiamo usare per avvicinarci abbastanza. Questo ci porta alla definizione di cui Orsopra:

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

Qui usiamo la tipizzazione strutturale e l' operatore libbra di Scala per creare un tipo strutturale Or[U,T]che è garantito per avere un tipo interno. Questa è una strana bestia. Per dare un certo contesto, la funzione def bar[X <: { type Y = Int }](x : X) = {}deve essere chiamata con sottoclassi AnyRefche hanno un tipo Ydefinito in esse:

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

L'uso dell'operatore libbra ci consente di fare riferimento al tipo interno Or[B, String]#pfe, usando la notazione infissa per l'operatore del tipo Or, arriviamo alla nostra definizione originale di foo:

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

Possiamo usare il fatto che i tipi di funzione sono contraddittori nel loro primo parametro di tipo al fine di evitare di definire il tratto Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 

Questo può risolvere il A|B <: A|B|Cproblema? stackoverflow.com/questions/45255270/… Non posso dirlo.
jhegedus,


7

Potresti dare un'occhiata a MetaScala , che ha qualcosa chiamato OneOf. Ho l'impressione che questo non funzioni bene con le matchdichiarazioni, ma che puoi simulare la corrispondenza usando le funzioni di ordine superiore. Dai un'occhiata a questo frammento , ad esempio, ma nota che la parte "corrispondenza simulata" è commentata, forse perché non funziona ancora del tutto.

Ora per un po 'di editoria: non credo che ci sia qualcosa di egregio nel definire Either3, Either4, ecc. Come descrivi. Questo è essenzialmente doppio rispetto ai 22 tipi di tupla standard incorporati in Scala. Sarebbe certamente bello se Scala avesse dei tipi disgiuntivi incorporati, e forse una bella sintassi per loro come {x, y, z}.


6

Sto pensando che il tipo disgiunto di prima classe è un supertipo sigillato, con sottotipi alternativi e conversioni implicite da / verso i tipi desiderati di disgiunzione a questi sottotipi alternativi.

Suppongo che questo indirizzi i commenti 33 - 36 della soluzione di Miles Sabin, quindi il tipo di prima classe che può essere impiegato sul sito di utilizzo, ma non l'ho provato.

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

Un problema è Scala sarà non impiegare nel contesto caso di corrispondenza, una conversione implicita da IntOfIntOrStringa Int(e StringOfIntOrStringper String), quindi deve definire estrattori e utilizzare case Int(i)invece case i : Int.


AGGIUNGI: Ho risposto a Miles Sabin sul suo blog come segue. Forse ci sono diversi miglioramenti su entrambi:

  1. Si estende a più di 2 tipi, senza alcun rumore aggiuntivo nel sito di utilizzo o definizione.
  2. Gli argomenti sono inscatolati implicitamente, ad esempio non è necessario size(Left(2))osize(Right("test")) .
  3. La sintassi della corrispondenza del modello è implicitamente unboxed.
  4. La boxe e unboxing possono essere ottimizzati dall'hotspot JVM.
  5. La sintassi potrebbe essere quella adottata da un futuro tipo di unione di prima classe, quindi la migrazione potrebbe forse essere senza soluzione di continuità? Forse per il nome del tipo di unione, sarebbe meglio usare Vinvece Or, ad esempio IntVString, ` Int |v| String`, ` Int or String` o il mio preferito ` Int|String`?

AGGIORNAMENTO: Segue la logica negazione della disgiunzione per il modello sopra, e ho aggiunto un modello alternativo (e probabilmente più utile) sul blog di 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)

UN ALTRO AGGIORNAMENTO: per quanto riguarda i commenti 23 e 35 della soluzione di Mile Sabin , ecco un modo per dichiarare un tipo di unione nel sito di utilizzo. Si noti che è unboxed dopo il primo livello, cioè ha il vantaggio di essere estensibile a qualsiasi numero di tipi nella disgiunzione , mentre ha Eitherbisogno di boxe nidificate e il paradigma nel mio precedente commento 41 non era estensibile. In altre parole, a D[Int ∨ String]è assegnabile a (cioè è un sottotipo di) 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]])
                ^

Apparentemente il compilatore Scala ha tre bug.

  1. Non sceglierà la funzione implicita corretta per nessun tipo dopo il primo tipo nella disgiunzione di destinazione.
  2. Non esclude il D[¬[Double]]caso dalla partita.

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

Il metodo get non è vincolato correttamente al tipo di input, perché il compilatore non lo consentirà Anella posizione covariante. Si potrebbe obiettare che si tratta di un bug perché tutto ciò che vogliamo è l'evidenza, non accediamo mai all'evidenza nella funzione. E ho fatto la scelta di non provare case _nel getmetodo, quindi non avrei dovuto deselezionare un Optionin matchin size().


05 marzo 2012: l'aggiornamento precedente deve essere migliorato. La soluzione di Miles Sabin ha funzionato correttamente con il sottotipo.

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

La proposta del mio precedente aggiornamento (per un tipo di unione quasi di prima classe) ha rotto il sottotitolo.

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

Il problema è che Adentro(() => A) => A appare sia nella posizione covariante (tipo di ritorno) sia in quella controversa (input di funzione, o in questo caso un valore di ritorno della funzione che è un input di funzione), quindi le sostituzioni possono essere solo invarianti.

Si noti che A => Nothingè necessario solo perché vogliamo Anella posizione controversa, in modo che i supertipi di A non siano sottotipi di D[¬[A]]D[¬[A] with ¬[U]]( vedi anche ). Dato che abbiamo solo bisogno di una doppia contraddizione, possiamo raggiungere l'equivalente della soluzione di Miles anche se possiamo scartare il ¬e .

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

Quindi la soluzione completa è.

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

Notare che rimangono i 2 bug precedenti in Scala, ma il terzo viene evitato in quanto Tè ora vincolato al sottotipo A.

Possiamo confermare i lavori di sottotipizzazione.

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

Ho pensato che i tipi intersezione di prima classe sono molto importanti, sia per i motivi Ceylon li ha , e perché invece di sussumere a Anyquali mezzi unboxing con una matchsui tipi che ci si attende in grado di generare un errore di runtime, l'unboxing di un ( collezione eterogenea contenente a) la disgiunzione può essere verificata (Scala deve correggere i bug che ho notato). I sindacati sono più semplici della complessità dell'uso della HList sperimentale di metascala per raccolte eterogenee.


L'elemento n. 3 sopra non è un bug nel compilatore Scala . Nota che inizialmente non l'avevo numerato come un bug, quindi oggi ho fatto una modifica con noncuranza e l'ho fatto (dimenticando la mia ragione originale per non affermare che fosse un bug). Non ho più modificato il post, perché sono al limite di 7 modifiche.
Shelby Moore III,

Il bug n. 1 sopra può essere evitato con una diversa formulazione della sizefunzione .
Shelby Moore III,

L'articolo n. 2 non è un bug. Scala non può esprimere completamente un tipo di unione . Il documento collegato fornisce un'altra versione del codice, in modo che sizenon accetti più D[Any]come input.
Shelby Moore III,

Non ho ben capito questa risposta, è presente anche una risposta a questa domanda: stackoverflow.com/questions/45255270/...
jhegedus

5

Esiste un altro modo che è leggermente più facile da capire se non si grugnisce 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

Uso una tecnica simile a Digione


Può funzionare con il sottotipo? Il mio istinto: no, ma potrei sbagliarmi. stackoverflow.com/questions/45255270/...
jhegedus

1

Bene, è tutto molto intelligente, ma sono abbastanza sicuro che tu sappia già che le risposte alle tue domande principali sono varie varietà di "No". Scala gestisce il sovraccarico in modo diverso e, bisogna ammetterlo, un po 'meno elegantemente di quanto tu descriva. Alcuni di questi sono dovuti all'interoperabilità di Java, altri sono dovuti al fatto di non voler colpire i casi smussati dell'algoritmo di inferenza del tipo, e altri perché semplicemente non sono Haskell.


5
Mentre uso Scala da un po 'di tempo, non sono né così esperto né intelligente come sembri pensare. In questo esempio, posso vedere come una libreria potrebbe fornire la soluzione. Ha quindi senso chiedersi se esiste una tale biblioteca (o qualche alternativa).
Aaron Novstrup,

1

Aggiungendo alle già ottime risposte qui. Ecco una sintesi che si basa sui tipi di unione di Miles Sabin (e sulle idee di Josh) ma li rende anche definiti in modo ricorsivo, quindi puoi avere> 2 tipi nell'unione (def foo[A : UNil Or Int Or String Or List[String] )

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

NB: dovrei aggiungere che dopo aver giocato con quanto sopra per un progetto, ho finito per tornare ai tipi di somma semplice (cioè tratto sigillato con sottoclassi). I tipi di unione Miles Sabin sono ottimi per limitare il parametro type, ma se è necessario restituire un tipo di unione, non offre molto.


Questo può risolvere il A|C <: A|B|Cproblema del sottotipo? stackoverflow.com/questions/45255270/… La mia sensazione è NO perché allora significherebbe che A or Cdovrebbe essere il sottotipo di (A or B) or Cma che non contiene il tipo, A or Cquindi non c'è speranza di fare almeno A or Cun sottotipo A or B or Ccon questa codifica. . cosa pensi ?
jhegedus,

0

Dai documenti , con l'aggiunta di 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

Per quanto riguarda la sealedparte:

È possibile definire ulteriori classi di casi che estendono il tipo Expr in altre parti del programma (...). Questa forma di estensibilità può essere esclusa dichiarando la classe base Expr sigillata; in questo caso, tutte le classi che estendono direttamente Expr devono trovarsi nello stesso file sorgente di Expr.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.