Jak zdefiniować „rozłączenie typu” (typy unii)?

181

Jednym ze sposobów zasugerowania podwójnej definicji przeciążonych metod jest zastąpienie przeciążenia dopasowaniem wzorca:

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

Takie podejście wymaga, abyśmy zrezygnowali ze statycznego sprawdzania typu argumentów dla foo. O wiele fajniej byłoby pisać

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

Mogę się zbliżyć Either, ale robi się brzydko szybko z więcej niż dwoma typami:

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

To wygląda jak ogólne (eleganckie, wydajne) rozwiązanie wymagałoby określającą Either3, Either4.... Czy ktoś wie alternatywnego rozwiązania dla osiągnięcia tego samego celu? Według mojej wiedzy Scala nie ma wbudowanego „rozróżnienia typów”. Ponadto, czy ukryte konwersje zdefiniowane powyżej czają się gdzieś w standardowej bibliotece, żebym mógł je po prostu zaimportować?

Aaron Novstrup
źródło

Odpowiedzi:

142

Cóż, w tym konkretnym przypadku Any*ta sztuczka poniżej nie zadziała, ponieważ nie będzie akceptować typów mieszanych. Ponieważ jednak typy mieszane również nie działają z przeciążeniem, może to być to, czego chcesz.

Najpierw zadeklaruj klasę z typami, które chcesz zaakceptować, jak poniżej:

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

Następnie zadeklaruj w footen sposób:

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

I to wszystko. Możesz zadzwonić foo(5)lub foo("abc"), a to zadziała, ale spróbuj foo(true)i zawiedzie. To może być ubocznym wszedł przez kod klienta przez stworzenie StringOrInt[Boolean], chyba że, jak zauważył Randall poniżej, dokonać StringOrIntsię sealedklasę.

Działa, ponieważ T: StringOrIntoznacza, że ​​istnieje niejawny parametr typu StringOrInt[T], a ponieważ Scala zagląda do obiektów towarzyszących typu, aby sprawdzić, czy istnieją niejawne warunki do działania kodu z pytaniem o ten typ.

Daniel C. Sobral
źródło
14
Jeśli class StringOrInt[T]tak się stanie sealed, „wyciek”, o którym mówiłeś („Oczywiście kod klienta może utworzyć krok po kroku, tworząc StringOrInt[Boolean]„), jest zatkany, przynajmniej jeśli StringOrIntznajduje się we własnym pliku. Następnie obiekty świadka muszą być zdefiniowane w tym samym sosie co StringOrInt.
Randall Schulz,
3
Próbowałem nieco uogólnić to rozwiązanie (opublikowane jako odpowiedź poniżej). Główną wadą w porównaniu z tym Eitherpodejściem wydaje się być to, że tracimy dużo obsługi kompilatora do sprawdzania dopasowania.
Aaron Novstrup
niezła sztuczka! Jednak nawet z zapieczętowaną klasą można nadal obchodzić ją w kodzie klienta, definiując domyślną wartość b = new StringOrInt [Boolean] w zakresie za pomocą foo, lub wywołując jawnie foo (2.9) (new StringOrInt [Double]). Myślę, że musisz też uczynić klasę abstrakcyjną.
Paolo Falabella
2
Tak; prawdopodobnie lepiej byłoby użyćtrait StringOrInt ...
Ślimak mechaniczny
7
Ps, jeśli chcesz obsługiwać podtypy, po prostu zmień StringOrInt[T]na StringOrInt[-T](patrz stackoverflow.com/questions/24387701/… )
Eran Medan
178

Miles Sabin opisuje bardzo fajny sposób na uzyskanie typu związku w swoim niedawnym wpisie na blogu Rozpakowane typy związków w Scali poprzez izomorfizm Curry-Howarda :

Najpierw określa negację typów jako

type ¬[A] = A => Nothing

stosowanie prawa De Morgana pozwala mu definiować typy związków

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

Z następującymi konstrukcjami pomocniczymi

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

możesz pisać typy unii w następujący sposób:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
źródło
13
To jedna z najbardziej niesamowitych rzeczy, jakie widziałem.
Submonoid,
18
Oto moja rozszerzona implementacja pomysłu Milesa: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - z przykładami: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ test /…
Connor Doyle,
6
Powyższy komentarz powinien być odpowiedzią samą w sobie. Jest to tylko implementacja pomysłu Milesa, ale ładnie zamknięta w pakiecie w Maven Central i bez wszystkich symboli Unicode, które mogłyby (?) Stanowić problem dla jakiegoś procesu kompilacji.
Jim Pivarski
2
Ta zabawna postać to boolowska negacja .
michid
1
Początkowo pomysł wydawał mi się zbyt zawiły. Czytając prawie każdy link wymieniony w tym wątku, wpadłem na pomysł i piękno jego realizacji :-) ... ale nadal mam wrażenie, że to jest coś skomplikowanego ... teraz tylko dlatego, że nie jest jeszcze dostępne prosto z dala od Scali. Jak mówi Miles: „Teraz musimy po prostu męczyć Martina i Adriaana, aby był bezpośrednio dostępny”.
Richard Gomes
44

Dotty , nowy eksperymentalny kompilator Scala, obsługuje typy związków (napisane A | B), dzięki czemu możesz robić dokładnie to, co chciałeś:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Samuel Gruetter
źródło
1
Jeden z tych dni.
Michael Ahlers,
5
Nawiasem mówiąc, Dotty będzie nową Scalą 3 (została ogłoszona kilka miesięcy temu).
6infinity8
1
i będą dostępne gdzieś pod koniec 2020 r.
JulienD
31

Oto sposób Rex Kerr do kodowania typów unii. Prosto i prosto!

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

Źródło: Komentarz nr 27 do tego doskonałego posta na blogu autorstwa Milesa Sabina, który zapewnia inny sposób kodowania typów związków w Scali.

missingfaktor
źródło
6
Niestety to kodowanie można pokonać: scala> f(9.2: AnyVal)przechodzi sprawdzanie typu.
Kipton Barros,
@Kipton: To smutne. Czy kodowanie Milesa Sabina również cierpi z powodu tego problemu?
missingfaktor
9
Istnieje nieco prostsza wersja kodu Milesa; skoro faktycznie używa odwrotnej implikacji przeciwstawnego parametru funkcji, a nie ścisłego „nie”, możesz użyć trait Contra[-A] {}zamiast wszystkich funkcji do niczego. Otrzymujesz rzeczy takie jak type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }używane def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(bez fantazyjnego Unicode).
Rex Kerr
Może to rozwiązać problem dziedziczenia typów związków? stackoverflow.com/questions/45255270/…
jhegedus
Hmm, próbowałem, nie mogę tworzyć typów zwrotów za pomocą tego kodowania, więc wydaje się, że nie jest możliwe zaimplementowanie podtypów stackoverflow.com/questions/45255270/…
jhegedus
18

Możliwe jest uogólnienie rozwiązania Daniela w następujący sposób:

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

Główne wady tego podejścia to

  • Jak zauważył Daniel, nie obsługuje kolekcji / varargs z typami mieszanymi
  • Kompilator nie wyświetla ostrzeżenia, jeśli dopasowanie nie jest wyczerpujące
  • Kompilator nie wyświetla błędu, jeśli dopasowanie obejmuje niemożliwą wielkość liter
  • Podobnie jak w Eitherpodejściu dalsze uogólnienie wymaga zdefiniowania analogiczne Or3, Or4itp cechy. Oczywiście zdefiniowanie takich cech byłoby znacznie prostsze niż zdefiniowanie odpowiednich Eitherklas.

Aktualizacja:

Mitch Blevins demonstruje bardzo podobne podejście i pokazuje, jak uogólnić na więcej niż dwa typy, nazywając to „jąkaniem lub”.

Aaron Novstrup
źródło
17

W pewnym sensie natknąłem się na względnie czystą implementację n-arskich typów unii, łącząc pojęcie list typów z uproszczeniem pracy Milesa Sabina w tym obszarze , o czym ktoś wspomina w innej odpowiedzi.

Biorąc pod uwagę typ, ¬[-A]który jest sprzeczny A, z definicji podając, A <: Bże możemy pisać ¬[B] <: ¬[A], odwracając kolejność typów.

Podane typy A, Bi Xchcemy wyrazić X <: A || X <: B. Stosujemy kontrowariancję ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. To z kolei może być wyrażone jako to, ¬[A] with ¬[B] <: ¬[X]w którym jednym z Alub Bmusi być nadtypem Xlub Xsamym sobą (pomyśl o argumentach funkcji).

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
}

Spędziłem trochę czasu, próbując połączyć ten pomysł z górną granicą typów elementów, jak widać na TLists harrah / up , jednak implementacja Mapz typami granic okazała się dotychczas trudna.

J Cracknell
źródło
1
To jest wspaniałe, dziękuję! Próbowałem wcześniejszych podejść, ale nadal miałem problemy z używaniem tego z typami rodzajowymi w ramach unii. To była jedyna implementacja, w której mogłem pracować z typami rodzajowymi.
Samer Adra,
Niestety, ale prawdopodobnie należy się tego spodziewać, gdy próbuję użyć metody Scala, która pobiera typ unii z kodu Java, nie działa. Błąd: (40, 29) java: metoda setValue w klasie Config nie może być zastosowana do podanych typów; wymagane: X, scala.Predef. $ mniej $ dwukropek $ mniej <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> znaleziono: java.lang. Przyczyna ciągnięcia: nie można wnioskować zmienne typu X (rzeczywista i formalna lista argumentów różni się długością)
Samer Adra
Nadal niejasne są niektóre szczegóły tego wdrożenia. Na przykład pierwotny artykuł zdefiniował negację jako „typ ¬ [A] = A => Nic”, ale w tej wersji, jeśli ma tylko „zapieczętowaną cechę ¬ [-A]”, a cecha ta nie jest nigdzie rozszerzona. Jak to działa?
Samer Adra
@Samer Adra To działałoby w obu przypadkach, artykuł wykorzystuje Function1jako istniejący typ kontrowariantny. Nie potrzebujesz implementacji, wszystko czego potrzebujesz to dowód zgodności ( <:<).
J Cracknell,
Masz pomysł, jak mieć konstruktor, który akceptuje typ unii?
Samer Adra
12

Rozwiązanie klasy typu jest prawdopodobnie najmilszym sposobem na skorzystanie z tej opcji. Jest to podobne do podejścia monoidu wspomnianego w książce 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)

Jeśli następnie uruchomisz to w 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
źródło
Mogę się mylić, ale nie sądzę, żeby tego właśnie szukał PO. OP pytał o typ danych, który mógłby reprezentować rozłączny związek typów, a następnie analizował na nim analizy przypadków w czasie wykonywania, aby zobaczyć, jaki był rzeczywisty typ. Klasy typów nie rozwiążą tego problemu, ponieważ są konstrukcją opartą wyłącznie na czasie kompilacji.
Tom Crockett,
4
Prawdziwe pytanie zadawane było jak wystawiać różne zachowanie dla różnych typów, ale bez przeciążania. Bez znajomości klas typów (i być może pewnej ekspozycji na C / C ++), typ unii wydaje się jedynym rozwiązaniem. Wcześniejszy Eithertyp Scali wzmacnia to przekonanie. Używanie klas typów za pośrednictwem implikacji Scali jest lepszym rozwiązaniem podstawowego problemu, ale jest to stosunkowo nowa koncepcja i wciąż mało znana, dlatego OP nawet nie wiedział, że uważa je za możliwą alternatywę dla typu unii.
Kevin Wright
czy to działa z subtypingiem? stackoverflow.com/questions/45255270/…
jhegedus
10

Chcielibyśmy operatora typu Or[U,V], które mogą być używane, aby ograniczyć do parametrów typu Xw taki sposób, że albo X <: Ualbo X <: V. Oto definicja, która jest tak bliska, jak to tylko możliwe:

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

Oto jak jest używany:

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

Wykorzystuje kilka sztuczek typu Scala. Głównym z nich jest zastosowanie uogólnionych ograniczeń typu . Biorąc pod uwagę typy Ui V, kompilator Scala zapewnia klasę o nazwie U <:< V(i niejawny obiekt tej klasy) tylko wtedy, gdy kompilator Scala może udowodnić, że Ujest to podtyp V. Oto prostszy przykład z uogólnionymi typami ograniczeń, który działa w niektórych przypadkach:

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

Ten przykład działa, gdy Xinstancja klasy B, a Stringlub ma typ, który nie jest ani podtypem, ani podtypem „ Blub” String. W pierwszych dwóch przypadkach, to prawda przez definicji withsłowa kluczowego, które (B with String) <: Bi (B with String) <: Stringtak Scala zapewni niejawny obiekt, który zostanie przekazany jako ev: kompilator Scala będzie poprawnie przyjąć foo[B]i foo[String].

W ostatnim przypadku polegam na tym, że jeśli U with V <: X, to U <: Xlub V <: X. Wydaje się to intuicyjnie prawdziwe i po prostu to zakładam. Z tego założenia wynika, że ​​ten prosty przykład zawodzi, gdy Xjest nadtypem lub podtypem jednego Blub String: na przykład w powyższym przykładzie foo[A]jest niepoprawnie zaakceptowany i foo[C]niepoprawnie odrzucony. Ponownie, co chcemy jest jakiś rodzaj ekspresji na zmiennych U, Vi Xto prawda, kiedy dokładnie X <: Ulub X <: V.

Pomysł Scali na sprzeczność może tu pomóc. Pamiętasz tę cechę trait Inv[-X]? Ponieważ jest sprzeczny w swoim parametrze typu X, Inv[X] <: Inv[Y]wtedy i tylko wtedy Y <: X. Oznacza to, że możemy zastąpić powyższy przykład przykładem, który faktycznie będzie działał:

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

Wynika to z tego, że wyrażenie (Inv[U] with Inv[V]) <: Inv[X]jest prawdziwe, przy takim samym założeniu powyżej, dokładnie kiedy Inv[U] <: Inv[X]lub Inv[V] <: Inv[X], a przez definicję kontrawariancji, jest to prawdziwe dokładnie wtedy, gdy X <: Ulub X <: V.

Można uczynić rzeczy nieco bardziej użytecznymi, deklarując parametr parametryzowalny BOrString[X]i używając go w następujący sposób:

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

Scala spróbuje skonstruować typu BOrString[X]dla każdego Xktóra foojest wywołana, a typ zostanie wykonana dokładnie, gdy Xjest podtypem albo Bczy String. To działa i istnieje skrótowy zapis. Poniższa składnia jest równoważna (z wyjątkiem tego, że evnależy się do niej odwoływać w treści metody, implicitly[BOrString[X]]a nie po prostu ev) i wykorzystuje BOrStringjako kontekst kontekstowy typu :

def foo[X : BOrString] = {}

To, co naprawdę chcielibyśmy, to elastyczny sposób na utworzenie powiązania kontekstu typu. Kontekst typu musi być parametryzowalnym typem, a my chcemy parametryzowalnego sposobu jego utworzenia. To brzmi, jakbyśmy próbowali wywoływać funkcje na typach, podobnie jak funkcje na wartościach. Innymi słowy, chcielibyśmy czegoś takiego:

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

Nie jest to bezpośrednio możliwe w Scali, ale istnieje pewien sposób na zbliżenie się. To prowadzi nas do powyższej definicji Or:

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

Tutaj używamy typowania strukturalnego i operatora funta Scali do tworzenia typu strukturalnego, Or[U,T]który ma zagwarantowany jeden typ wewnętrzny. To dziwna bestia. Aby podać kontekst, def bar[X <: { type Y = Int }](x : X) = {}należy wywołać funkcję z podklasami o zdefiniowanym AnyReftypie Y:

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

Korzystanie z operatora funta pozwala nam odnosić się do typu wewnętrznego Or[B, String]#pf, a przy użyciu notacji infix dla operatora typu Ordochodzimy do naszej oryginalnej definicji foo:

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

Możemy wykorzystać fakt, że typy funkcji są sprzeczne w swoim pierwszym parametrze typu, aby uniknąć zdefiniowania cechy Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
Josh
źródło
Czy to może rozwiązać A|B <: A|B|Cproblem? stackoverflow.com/questions/45255270/… Nie mogę powiedzieć.
jhegedus
7

Możesz rzucić okiem na MetaScala , która ma coś o nazwie OneOf. Mam wrażenie, że nie działa to dobrze z matchinstrukcjami, ale można symulować dopasowanie za pomocą funkcji wyższego rzędu. Na przykład spójrz na ten fragment kodu, ale zauważ, że część „symulowanego dopasowywania” jest komentowana, być może dlatego, że jeszcze nie działa.

A teraz trochę redakcji: nie wydaje mi się, żeby w opisywaniu Either3, Either4 itd. Było coś rażącego. Jest to zasadniczo podwójne w stosunku do standardowych 22 krotek wbudowanych w Scalę. Byłoby na pewno fajnie, gdyby Scala miał wbudowane typy rozłączne i być może dla nich jakąś fajną składnię {x, y, z}.

Tom Crockett
źródło
6

Myślę, że rozłącznym typem pierwszej klasy jest zapieczętowany nadtyp, z alternatywnymi podtypami i niejawnymi konwersjami do / z pożądanych typów rozłączenia na te alternatywne podtypy.

Zakładam, że odnosi się to do komentarzy 33 - 36 rozwiązania Milesa Sabina, więc jest to typ pierwszej klasy, który można zastosować na stronie użytkowania, ale go nie przetestowałem.

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

Jednym z problemów jest to, że Scala nie zastosuje w przypadku dopasowania kontekstu, niejawnej konwersji z IntOfIntOrStringna Int(i StringOfIntOrStringna String), więc należy zdefiniować ekstraktory i używać case Int(i)zamiast case i : Int.


DODAJ: Odpowiedziałem Milesowi Sabinowi na jego blogu w następujący sposób. Być może istnieje kilka ulepszeń w stosunku do któregokolwiek:

  1. Rozciąga się na więcej niż 2 typy, bez dodatkowego hałasu w miejscu użytkowania lub definicji.
  2. Argumenty są umieszczane domyślnie, np. Nie trzeba size(Left(2))lub size(Right("test")).
  3. Składnia dopasowania wzorca jest niejawnie rozpakowana.
  4. Boksowanie i rozpakowywanie może być zoptymalizowane przez hotspot JVM.
  5. Składnia może być przyjęta przez przyszły pierwszy typ unii, więc migracja może być płynna? Być może dla nazwy typu unii lepiej byłoby użyć Vzamiast Ornp. IntVString` Int |v| String`, ` Int or String` Lub mojego ulubionego ` Int|String`?

AKTUALIZACJA: Następuje logiczna negacja niezgodności powyższego wzoru, a ja dodałem alternatywny (i prawdopodobnie bardziej użyteczny) wzór na blogu Milesa Sabina .

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)

KOLEJNA AKTUALIZACJA: Jeśli chodzi o komentarze 23 i 35 dotyczące rozwiązania Mile Sabin , oto sposób zadeklarowania typu związku na stronie użytkowania. Zauważ, że jest rozpakowywany po pierwszym poziomie, tj. Ma tę zaletę, że może być rozszerzany na dowolną liczbę typów w rozłączeniu , podczas gdy Eitherwymaga zagnieżdżenia boksu, a paradygmat w moim poprzednim komentarzu 41 nie był rozszerzalny. Innymi słowy, a D[Int ∨ String]można przypisać (tj. Jest podtypem) 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]])
                ^

Najwyraźniej kompilator Scala ma trzy błędy.

  1. Nie wybierze prawidłowej funkcji niejawnej dla dowolnego typu po pierwszym typie w rozłączeniu docelowym.
  2. Nie wyklucza to D[¬[Double]]przypadku z meczu.

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

Metoda get nie jest poprawnie ograniczona do typu danych wejściowych, ponieważ kompilator nie pozwala Ana pozycję kowariantną. Można argumentować, że jest to błąd, ponieważ wszystko czego chcemy to dowód, nigdy nie uzyskujemy dostępu do dowodów w funkcji. I dokonał wyboru nie do badania case _w getsposób, więc nie będzie musiał unbox Optionw matchw size().


05 marca 2012: poprzednia aktualizacja wymaga ulepszenia. Rozwiązanie Milesa Sabina działało poprawnie z podtytułem.

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

Moja poprzednia propozycja aktualizacji (dla typu unii prawie pierwszej klasy) złamała podtytuł.

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

Problem polega na tym, że Ain (() => A) => Apojawia się zarówno w pozycjach kowariantnej (typ zwracany), jak i kontrawariantnej (wejście funkcji, lub w tym przypadku wartość zwracana funkcji, która jest wejściem funkcji), dlatego podstawienia mogą być tylko niezmienne.

Należy pamiętać, że A => Nothingjest to konieczne tylko dlatego, że chcemy Aw pozycji kontrawariantny, tak że supertypes z A nie podtypów z D[¬[A]]ani D[¬[A] with ¬[U]]( patrz również ). Ponieważ potrzebujemy tylko podwójnej sprzeczności, możemy osiągnąć równowartość rozwiązania Milesa, nawet jeśli możemy odrzucić ¬i .

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

Tak więc pełna poprawka jest.

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

Zwróć uwagę, że pozostały 2 poprzednie błędy w Scali, ale trzeciego unika się, ponieważ Tobecnie jest ograniczony do podtypu A.

Możemy potwierdzić prace podtypów.

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

Myślałem, że typy przecięcia pierwszej klasy są bardzo ważne, zarówno dla powodów Ceylon je ma , i dlatego zamiast podporządkowania do Anyjakich środków unboxing z matchoczekiwanych typów może wygenerować błąd wykonywania, unboxing o ( kolekcja heterogenicznym zawierającym a) rozłączność można sprawdzić pod kątem typu (Scala musi naprawić błędy, które zauważyłem). Związki są bardziej proste niż złożoności przy użyciu eksperymentalnego HList z metascala dla zbiorów heterogenicznych.

Shelby Moore III
źródło
Punkt 3 powyżej nie jest błędem w kompilatorze Scala . Zauważ, że pierwotnie nie numerowałem tego jako błędu, ale dzisiaj niedbale dokonałem edycji i zrobiłem to (zapominając o moim pierwotnym powodzie, dla którego nie stwierdziłem, że to błąd). Nie edytowałem posta ponownie, ponieważ mam limit 7 edycji.
Shelby Moore III
Błąd nr 1 powyżej można uniknąć, stosując inne sformułowanie tej sizefunkcji .
Shelby Moore III
Przedmiot nr 2 nie jest błędem. Scala nie może w pełni wyrazić typu związku . Połączony dokument zapewnia inną wersję kodu, dzięki czemu sizenie jest już akceptowany D[Any]jako dane wejściowe.
Shelby Moore III
Nie do końca dostaję tę odpowiedź, czy jest to również odpowiedź na to pytanie: stackoverflow.com/questions/45255270/…
jhegedus
5

Jest inny sposób, który jest nieco łatwiejszy do zrozumienia, jeśli nie grokujesz Curry-Howarda:

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

Używam podobnej techniki w Dijon

pathikrit
źródło
Czy to może działać z subtypingiem? Moje przeczucie: nie, ale mogę się mylić. stackoverflow.com/questions/45255270/…
jhegedus
1

Cóż, to wszystko jest bardzo sprytne, ale jestem pewien, że już wiesz, że odpowiedzi na twoje wiodące pytania to różne odmiany „Nie”. Scala radzi sobie z przeciążeniem w różny sposób i, trzeba przyznać, nieco mniej elegancko niż to opisujesz. Niektóre z nich wynikają z interoperacyjności Javy, niektóre z tego wynikają z tego, że nie chcą trafiać w przypadki algorytmu wnioskowania typu, a niektóre z tego powodu nie są po prostu Haskellem.

Dave Griffith
źródło
5
Podczas gdy używam Scali od jakiegoś czasu, nie jestem ani tak kompetentny, ani mądry, jak się wydaje. W tym przykładzie widzę, jak biblioteka może zapewnić rozwiązanie. Sensowne jest wtedy zastanawianie się, czy taka biblioteka istnieje (lub jakaś alternatywa).
Aaron Novstrup
1

Dodając do już świetnych odpowiedzi tutaj. Oto podstawa, która opiera się na typach związków Milesa Sabina (i pomysłach Josha), ale także sprawia, że ​​są one rekurencyjnie zdefiniowane, dzięki czemu możesz mieć> 2 typy w związku ( def foo[A : UNil Or Int Or String Or List[String])

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

NB: Powinienem dodać, że po przeanalizowaniu powyższego projektu, w końcu wróciłem do zwykłych starych sum (tj. Zapieczętowanej cechy z podklasami). Typy złączy Milesa Sabina świetnie nadają się do ograniczania parametru type, ale jeśli musisz zwrócić typ związku, nie oferuje on wiele.

Aish
źródło
Czy to może rozwiązać A|C <: A|B|Cproblem z podtytułem? stackoverflow.com/questions/45255270/... Moje jelita czują się NIE, ponieważ wtedy oznaczałoby to, że A or Cmusiałby to być podtyp, (A or B) or Cale nie zawiera tego typu, A or Cwięc nie ma nadziei na zrobienie A or Cpodtypu A or B or Cz tym kodowaniem przynajmniej .. . co myślisz ?
jhegedus
0

Z dokumentów , z dodatkiem 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

W odniesieniu do sealedczęści:

Możliwe jest zdefiniowanie dalszych klas spraw, które rozszerzają typ Expr w innych częściach programu (...). Tę formę rozszerzalności można wykluczyć, deklarując, że klasa bazowa Expr jest zapieczętowana; w takim przypadku wszystkie klasy bezpośrednio rozszerzające Expr muszą znajdować się w tym samym pliku źródłowym co Expr.

Elazar
źródło