¿Cómo definir "disyunción de tipo" (tipos de unión)?

181

Una forma sugerida para tratar con las definiciones dobles de métodos sobrecargados es reemplazar la sobrecarga con la coincidencia de patrones:

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

Este enfoque requiere que entreguemos la comprobación de tipo estático en los argumentos foo. Sería mucho mejor poder escribir

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

Puedo acercarme Either, pero se pone feo rápido con más de dos tipos:

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

Se ve como una solución general (elegante, eficiente) requeriría definir Either3, Either4, .... ¿Alguien sabe de una solución alternativa para lograr el mismo fin? Que yo sepa, Scala no tiene "disyunción de tipo" incorporada. Además, ¿están las conversiones implícitas definidas anteriormente al acecho en la biblioteca estándar en algún lugar para que pueda importarlas?

Aaron Novstrup
fuente

Respuestas:

142

Bueno, en el caso específico de Any*, este truco a continuación no funcionará, ya que no aceptará tipos mixtos. Sin embargo, dado que los tipos mixtos tampoco funcionarían con la sobrecarga, esto puede ser lo que desea.

Primero, declare una clase con los tipos que desea aceptar de la siguiente manera:

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

A continuación, declara fooasí:

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

Y eso es. Puede llamar foo(5)o foo("abc"), y funcionará, pero intente foo(true)y fallará. El código del cliente podría evitarlo creando un StringOrInt[Boolean], a menos que, como lo indica Randall a continuación, realice StringOrIntuna sealedclase.

Funciona porque T: StringOrIntsignifica que hay un parámetro implícito de tipo StringOrInt[T], y porque Scala busca dentro de los objetos complementarios de un tipo para ver si hay implicidades allí para que el código que solicita ese tipo funcione.

Daniel C. Sobral
fuente
14
Si class StringOrInt[T]se realiza sealed, la "fuga" a la que se refirió ("Por supuesto, esto podría ser eludido por el código del cliente creando un StringOrInt[Boolean]") se conecta, al menos si StringOrIntreside en un archivo propio. Entonces los objetos testigos deben definirse en la misma fuente que StringOrInt.
Randall Schulz
3
Intenté generalizar algo esta solución (publicado como respuesta a continuación). El principal inconveniente en comparación con el Eitherenfoque parece ser que perdemos mucho soporte del compilador para verificar la coincidencia.
Aaron Novstrup
¡buen truco! Sin embargo, incluso con la clase sellada, aún puede eludirla en el código del cliente ya sea definiendo un valor implícito val b = new StringOrInt [Boolean] en alcance con foo, o llamando explícitamente foo (2.9) (new StringOrInt [Double]). Creo que también debes hacer que la clase sea abstracta.
Paolo Falabella
2
Si; probablemente sería mejor usartrait StringOrInt ...
Caracol mecánico
77
Ps si desea admitir subtipos, simplemente cambie StringOrInt[T]a StringOrInt[-T](consulte stackoverflow.com/questions/24387701/… )
Eran Medan
178

Miles Sabin describe una forma muy agradable de obtener el tipo de unión en su reciente publicación de blog Tipos de unión sin caja en Scala a través del isomorfismo de Curry-Howard :

Primero define la negación de tipos como

type ¬[A] = A => Nothing

usando la ley de De Morgan esto le permite definir tipos de unión

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

Con las siguientes construcciones auxiliares

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

puede escribir tipos de unión de la siguiente manera:

def size[T : (Int || String)#λ](t : T) = t match {
    case i : Int => i
    case s : String => s.length
}
michid
fuente
13
Esa es una de las cosas más increíbles que he visto.
Submonoide
18
Aquí está mi implementación extendida de la idea de Miles: github.com/GenslerAppsPod/scalavro/blob/master/util/src/main/… - con ejemplos: github.com/GenslerAppsPod/scalavro/blob/master/util/src/ prueba / ...
Connor Doyle
66
El comentario anterior debería ser una respuesta por sí solo. Es solo una implementación de la idea de Miles, pero bien envuelta en un paquete en Maven Central, y sin todos esos símbolos unicode que podrían (?) Plantear un problema para algo en un proceso de construcción en alguna parte.
Jim Pivarski el
2
Ese personaje divertido es la negación booleana .
michid
1
Inicialmente, la idea me pareció demasiado complicada. Al leer casi todos los enlaces mencionados en este hilo, me sorprendió la idea y la belleza de su implementación :-) ... pero todavía siento que esto es algo complicado ... ahora solo porque aún no está disponible directamente lejos de Scala. Como dice Miles: "Ahora solo tenemos que molestar a Martin y Adriaan para que sea directamente accesible".
Richard Gomes
44

Dotty , un nuevo compilador experimental de Scala, admite tipos de unión (escritos A | B), por lo que puede hacer exactamente lo que quería:

def foo(xs: (String | Int)*) = xs foreach {
   case _: String => println("str")
   case _: Int => println("int")
}
Samuel Gruetter
fuente
1
Uno de estos días.
Michael Ahlers
55
Por cierto, Dotty será el nuevo scala 3 (se anunció hace unos meses).
6infinity8
1
y estará disponible en algún lugar a fines de 2020
JulienD
31

Aquí está la forma de Rex Kerr para codificar tipos de unión. ¡Directo y simple!

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

Fuente: Comentario # 27 en esta excelente publicación de blog de Miles Sabin que proporciona otra forma de codificar tipos de unión en Scala.

missingfaktor
fuente
66
Desafortunadamente, esta codificación puede ser derrotada: scala> f(9.2: AnyVal)pasa el typechecker.
Kipton Barros
@Kipton: Eso es triste. ¿La codificación de Miles Sabin también sufre este problema?
missingfaktor
9
Hay una versión un poco más simple del código de Miles; dado que en realidad usa la implicación inversa del parámetro contravariante de la función, no un estricto "no", puede usarlo trait Contra[-A] {}en lugar de todas las funciones para nada. Entonces obtienes cosas como type Union[A,B] = { type Check[Z] = Contra[Contra[Z]] <:< Contra[Contra[A] with Contra[B]] }usadas def f[T: Union[Int, String]#Check](t: T) = t match { case i: Int => i; case s: String => s.length }(sin unicode elegante).
Rex Kerr
¿Esto podría resolver el problema de herencia de los tipos de unión? stackoverflow.com/questions/45255270/…
jhegedus
Hmm, lo intenté, no puedo crear tipos de retorno con estas codificaciones, por lo que no parece posible implementar el subtipo stackoverflow.com/questions/45255270/…
jhegedus
18

Es posible generalizar la solución de Daniel de la siguiente manera:

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

Los principales inconvenientes de este enfoque son

  • Como señaló Daniel, no maneja colecciones / varargs con tipos mixtos
  • El compilador no emite una advertencia si la coincidencia no es exhaustiva
  • El compilador no emite un error si la coincidencia incluye un caso imposible
  • Al igual que el Eitherenfoque, más generalización requeriría la definición análoga Or3, Or4, etc. rasgos. Por supuesto, definir tales rasgos sería mucho más simple que definir las Eitherclases correspondientes .

Actualizar:

Mitch Blevins demuestra un enfoque muy similar y muestra cómo generalizarlo a más de dos tipos, denominándolo "tartamudeo".

Aaron Novstrup
fuente
18

Me he topado con una implementación relativamente limpia de los tipos de unión n-aria al combinar la noción de listas de tipos con una simplificación del trabajo de Miles Sabin en esta área , que alguien menciona en otra respuesta.

Dado el tipo ¬[-A]que es contravariante A, por definición A <: B, podemos escribir ¬[B] <: ¬[A], invirtiendo el orden de los tipos.

Tipos dados A, By X, que quieren expresar X <: A || X <: B. Aplicando contravarianza, obtenemos ¬[A] <: ¬[X] || ¬[B] <: ¬[X]. Esto a su vez puede ser expresado como ¬[A] with ¬[B] <: ¬[X]en la que uno de Ao Bdebe ser un subtipo de él Xo de Xella misma (pensar en argumentos de la función).

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
}

Pasé algún tiempo tratando de combinar esta idea con un límite superior en los tipos de miembros como se ve en el TLists de harrah / up , sin embargo, la implementación de Mapcon límites de tipo hasta ahora ha resultado un desafío.

J Cracknell
fuente
1
Esto es genial, gracias! Probé los enfoques anteriores, pero seguí teniendo problemas para usar esto con tipos genéricos como parte de la unión. Esta fue la única implementación que pude trabajar con tipos genéricos.
Samer Adra
Lamentablemente, pero probablemente sea de esperar, cuando trato de usar un método Scala que toma un tipo de unión del código Java, no funciona. Error: (40, 29) java: el método setValue en la clase Config no se puede aplicar a tipos dados; requerido: X, scala.Predef. $ less $ colon $ less <UnionTypes.package. $ u00AC <java.lang.Object>, UnionTypes.package. $ u00AC <X>> encontrado: java.lang. Cadena: no se puede inferir tipo-variable (s) X (las listas de argumentos reales y formales difieren en longitud)
Samer Adra
Todavía no está del todo claro sobre algunos de los detalles en esta implementación. Por ejemplo, el artículo original definió la negación como "tipo ¬ [A] = A => Nada" pero en esta versión si solo tiene "rasgo sellado ¬ [-A]" y el rasgo no se extiende a ninguna parte. ¿Como funciona esto?
Samer Adra
@Samer Adra Funcionaría de cualquier manera, el artículo se usa Function1como un tipo contravariante existente. No necesita una implementación, todo lo que necesita es evidencia de conformidad ( <:<).
J Cracknell
¿Alguna idea de cómo tener un constructor que acepte un tipo de unión?
Samer Adra
13

Una solución de clase de tipo es probablemente la mejor manera de ir aquí, usando implicits. Esto es similar al enfoque monoide mencionado en el libro 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)

Si luego ejecuta esto en 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
fuente
Podría estar equivocado, pero no creo que esto sea lo que estaba buscando el OP. OP estaba preguntando acerca de un tipo de datos que podría representar una unión disjunta de tipos, y luego hacer un análisis de casos en tiempo de ejecución para ver cuál resultó ser el tipo real. Las clases de tipos no resolverán este problema, ya que son una construcción puramente en tiempo de compilación.
Tom Crockett
55
La verdadera pregunta que se hizo fue cómo exponer diferentes comportamientos para diferentes tipos, pero sin sobrecargar. Sin el conocimiento de las clases de tipos (y quizás alguna exposición a C / C ++), un tipo de unión parece ser la única solución. El Eithertipo preexistente de Scala tiende a reforzar esta creencia. El uso de clases de tipos a través de los implícitos de Scala es una mejor solución al problema subyacente, pero es un concepto relativamente nuevo y aún no ampliamente conocido, por lo que el OP ni siquiera sabía considerarlos como una posible alternativa a un tipo de unión.
Kevin Wright el
funciona esto con subtipo? stackoverflow.com/questions/45255270/…
jhegedus
10

Nos gustaría un operador de tipo Or[U,V]que se pueda utilizar para restringir los parámetros de un tipo Xde tal manera que X <: Uo X <: V. Aquí hay una definición que se acerca lo más posible:

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

Así es como se usa:

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

Esto usa algunos trucos de tipo Scala. El principal es el uso de restricciones de tipo generalizadas . Dados tipos Uy V, el compilador Scala proporciona una clase llamada U <:< V(y un objeto implícito de esa clase) si y solo si el compilador Scala puede probar que Ues un subtipo de V. Aquí hay un ejemplo más simple que usa restricciones de tipo generalizadas que funciona para algunos casos:

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

Este ejemplo funciona cuando Xuna instancia de clase B, a Stringo tiene un tipo que no es ni un supertipo ni un subtipo de Bo String. En los primeros dos casos, es cierto por la definición de la withpalabra clave que (B with String) <: By (B with String) <: String, por lo que Scala proporcionará un objeto implícito que se pasará como ev: el compilador de Scala aceptará correctamente foo[B]yfoo[String] .

En el último caso, estoy confiando en el hecho de que si U with V <: X, entonces U <: Xo V <: X. Parece intuitivamente cierto, y simplemente lo estoy asumiendo. A partir de esta suposición, está claro por qué este ejemplo simple falla cuando Xes un supertipo o subtipo de cualquiera Bo String: por ejemplo, en el ejemplo anterior, foo[A]se acepta incorrectamente y foo[C]se rechaza incorrectamente. Una vez más, lo que queremos es una especie de expresión de tipo de las variables U, Vy Xque es cierto exactamente cuándo X <: Uo X <: V.

La noción de contravarianza de Scala puede ayudar aquí. ¿Recuerdas el rasgo trait Inv[-X]? Porque es contravariante en su parámetro de tipo X, Inv[X] <: Inv[Y]si y solo si Y <: X. Eso significa que podemos reemplazar el ejemplo anterior con uno que realmente funcione:

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

Esto se debe a que la expresión (Inv[U] with Inv[V]) <: Inv[X]es verdadera, por el mismo supuesto anterior, exactamente cuándo Inv[U] <: Inv[X]o Inv[V] <: Inv[X], y por la definición de contravarianza, esto es cierto exactamente cuándo X <: Uo X <: V.

Es posible hacer las cosas un poco más reutilizables declarando un tipo parametrizable BOrString[X]y usándolo de la siguiente manera:

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

Scala ahora intentará construir el tipo BOrString[X]para cada Xque foose llama con, y el tipo será construido precisamente cuando Xes un subtipo de cualquiera Bo String. Eso funciona, y hay una notación abreviada. La siguiente sintaxis es equivalente (excepto que evahora se debe hacer referencia en el cuerpo del método como implicitly[BOrString[X]]algo más que simple ev) y se utiliza BOrStringcomo un tipo de contexto vinculado :

def foo[X : BOrString] = {}

Lo que realmente nos gustaría es una forma flexible de crear un tipo de contexto vinculado. Un contexto de tipo debe ser un tipo parametrizable, y queremos una forma parametrizable de crear uno. Eso suena como que estamos tratando de curry funciones en tipos al igual que curry funciones en valores. En otras palabras, nos gustaría algo como lo siguiente:

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

Eso no es directamente posible en Scala, pero hay un truco que podemos usar para acercarnos bastante. Eso nos lleva a la definición de Orarriba:

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

Aquí usamos la tipificación estructural y el operador de libra de Scala para crear un tipo estructural Or[U,T]que garantiza tener un tipo interno. Esta es una bestia extraña. Para dar algo de contexto, la función def bar[X <: { type Y = Int }](x : X) = {}debe llamarse con subclases AnyRefque tengan un tipo Ydefinido en ellas:

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

El uso del operador de libra nos permite referirnos al tipo interno Or[B, String]#pf, y al usar la notación infija para el operador de tipo Or, llegamos a nuestra definición original de foo:

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

Podemos utilizar el hecho de que los tipos de función son contravariantes en su primer parámetro de tipo para evitar definir el rasgo Inv:

type Or[U,T] = {
    type pf[X] = ((U => _) with (T => _)) <:< (X => _)
} 
Josh
fuente
¿Puede esto resolver el A|B <: A|B|Cproblema? stackoverflow.com/questions/45255270/… No puedo decirlo.
jhegedus
7

Puede echar un vistazo a MetaScala , que tiene algo llamado OneOf. Tengo la impresión de que esto no funciona bien con las matchdeclaraciones, pero que puede simular coincidencias utilizando funciones de orden superior. Echa un vistazo a este fragmento , por ejemplo, pero tenga en cuenta que la parte de "coincidencia simulada" está comentada, tal vez porque todavía no funciona del todo.

Ahora para un poco de editorialización: no creo que haya nada atroz en definir Either3, Either4, etc., como usted describe. Esto es esencialmente dual a los 22 tipos de tuplas estándar integrados en Scala. Ciertamente sería bueno si Scala tuviera tipos disyuntivos incorporados, y tal vez alguna buena sintaxis para ellos {x, y, z}.

Tom Crockett
fuente
6

Estoy pensando que el tipo disjunto de primera clase es un supertipo sellado, con los subtipos alternativos y las conversiones implícitas a / desde los tipos deseados de la disyunción a estos subtipos alternativos.

Supongo que esto aborda los comentarios 33 a 36 de la solución de Miles Sabin, por lo que es el tipo de primera clase que se puede emplear en el sitio de uso, pero no lo probé.

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 es que Scala no empleará en el contexto de coincidencia de casos, una conversión implícita de IntOfIntOrStringa Int(y StringOfIntOrStringa String), por lo que debe definir extractores y usar en case Int(i)lugar de case i : Int.


AGREGAR: respondí a Miles Sabin en su blog de la siguiente manera. Quizás hay varias mejoras sobre cualquiera de los dos:

  1. Se extiende a más de 2 tipos, sin ningún ruido adicional en el sitio de uso o definición.
  2. Los argumentos están encuadrados implícitamente, por ejemplo, no necesita size(Left(2))o size(Right("test")).
  3. La sintaxis de la coincidencia de patrones está implícitamente sin caja.
  4. El punto de acceso de JVM puede optimizar el boxeo y el desempaquetado.
  5. La sintaxis podría ser la adoptada por un futuro tipo de sindicato de primera clase, por lo que la migración podría ser perfecta. Tal vez para el nombre del tipo de unión, sería mejor usar en Vlugar de Or, por ejemplo IntVString, ` Int |v| String`, ` Int or String` o mi favorito `Int|String '?

ACTUALIZACIÓN: Sigue la negación lógica de la disyunción para el patrón anterior, y agregué un patrón alternativo (y probablemente más útil) en el blog de 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)

OTRA ACTUALIZACIÓN: Con respecto a los comentarios 23 y 35 de la solución de Mile Sabin , aquí hay una manera de declarar un tipo de unión en el sitio de uso. Tenga en cuenta que no está en caja después del primer nivel, es decir, tiene la ventaja de ser extensible a cualquier tipo de tipos en la disyunción , mientras que Eithernecesita un boxeo anidado y el paradigma en mi comentario anterior 41 no era extensible. En otras palabras, a D[Int ∨ String]es asignable a (es decir, es un subtipo de) 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]])
                ^

Aparentemente, el compilador Scala tiene tres errores.

  1. No elegirá la función implícita correcta para ningún tipo después del primer tipo en la disyunción de destino.
  2. No excluye el D[¬[Double]]caso del partido.

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

El método get no está restringido adecuadamente en el tipo de entrada, porque el compilador no permitirá Ala posición covariante. Uno podría argumentar que es un error porque todo lo que queremos es evidencia, nunca accedemos a la evidencia en la función. Y tomé la decisión de no probar case _en el getmétodo, para no tener que desempaquetar un Optionen el matchin size().


05 de marzo de 2012: la actualización previa necesita una mejora. La solución de Miles Sabin funcionó correctamente con el subtipo.

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 propuesta de mi actualización anterior (para un tipo de sindicato cercano a la primera clase) rompió el subtipo.

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

El problema es que Aen(() => A) => A aparece tanto en la covariante (tipo de retorno) y contravariant (entrada de la función, o en este caso un valor de retorno de la función que es una entrada de la función) Las posiciones, por lo tanto sustituciones pueden solamente ser invariante.

Tenga en cuenta que A => Nothinges necesario solo porque queremos Aen la posición contravariante, de modo que los supertipos de A no sean subtipos de D[¬[A]]ni D[¬[A] with ¬[U]]( ver también ). Como solo necesitamos una doble contravarianza, podemos lograr el equivalente a la solución de Miles, incluso si podemos descartar el ¬y .

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

Entonces la solución completa es.

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

Tenga en cuenta que los 2 errores anteriores en Scala permanecen, pero el tercero se evita ya Tque ahora está limitado a ser un subtipo deA .

Podemos confirmar que el subtipo funciona.

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

He estado pensando que los tipos de intersección de primera clase son muy importantes, tanto por las razones Ceilán ellos tiene , y porque en lugar de subsumir a Anylos cuales medios unboxing con un matchsobre tipos esperados puede generar un error de ejecución, el unboxing de un ( colección heterogénea que contiene a) la disyunción puede verificarse (Scala tiene que corregir los errores que señalé) Las uniones son más directas que la complejidad de usar la HList experimental de metascala para colecciones heterogéneas.

Shelby Moore III
fuente
El elemento # 3 anterior no es un error en el compilador Scala . Tenga en cuenta que originalmente no lo había numerado como un error, luego hice una edición descuidadamente hoy y lo hice (olvidando mi razón original para no afirmar que era un error). No volví a editar la publicación, porque estoy en el límite de 7 ediciones.
Shelby Moore III
El error n. ° 1 anterior se puede evitar con una formulación diferente de la sizefunción .
Shelby Moore III
El elemento # 2 no es un error. Scala no puede expresar completamente un tipo de unión . El documento vinculado proporciona otra versión del código, por lo que sizeya no acepta D[Any]como entrada.
Shelby Moore III
No acabo de entender esto respuesta, esto también es una respuesta a esta pregunta: ¿ stackoverflow.com/questions/45255270/...
jhegedus
5

Hay otra forma que es un poco más fácil de entender si no asimilas a 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

Yo uso una técnica similar en dijon

pathikrit
fuente
¿Puede esto funcionar con subtipos? Mi instinto: no, pero podría estar equivocado. stackoverflow.com/questions/45255270/…
jhegedus
1

Bueno, eso es todo muy inteligente, pero estoy bastante seguro de que ya sabe que las respuestas a sus preguntas principales son varias variedades de "No". Scala maneja la sobrecarga de manera diferente y, debe admitirse, algo menos elegante de lo que usted describe. Parte de eso se debe a la interoperabilidad de Java, parte de eso se debe a que no se quiere llegar a los casos afilados del algoritmo de inferencia de tipo, y parte de eso se debe simplemente a que simplemente no es Haskell.

Dave Griffith
fuente
55
Si bien he estado usando Scala por un tiempo, no soy tan conocedor ni tan inteligente como parece. En este ejemplo, puedo ver cómo una biblioteca podría proporcionar la solución. Tiene sentido preguntarse si existe tal biblioteca (o alguna alternativa).
Aaron Novstrup
1

Agregando a las respuestas ya excelentes aquí. Aquí hay una idea general que se basa en los tipos de unión de Miles Sabin (y las ideas de Josh) pero también los define de forma recursiva, por lo que puede tener> 2 tipos en la unión (def foo[A : UNil Or Int Or String Or List[String] )

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

NB: debo agregar que después de jugar con lo anterior para un proyecto, terminé volviendo a los tipos de suma simple (es decir, rasgo sellado con subclases). Los tipos de unión de Miles Sabin son excelentes para restringir el parámetro de tipo, pero si necesita devolver un tipo de unión, entonces no ofrece mucho.

Aish
fuente
¿Puede esto resolver el A|C <: A|B|Cproblema de subtipo? stackoverflow.com/questions/45255270/… Mi instinto se siente NO porque entonces significaría que A or Ctendría que ser el subtipo de (A or B) or Cpero que no contiene el tipo, A or Cpor lo que no hay esperanza de hacer A or Cun subtipo de A or B or Cal menos esta codificación ... . Qué piensas ?
jhegedus
0

De los documentos , con la adición de 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

Sobre la sealedparte:

Es posible definir más clases de casos que extiendan el tipo Expr en otras partes del programa (...). Esta forma de extensibilidad se puede excluir declarando la clase base Expr sellada; en este caso, todas las clases que extienden directamente Expr deben estar en el mismo archivo fuente que Expr.

Elazar
fuente