Desactivar selectivamente la subsunción en Scala?(escribe correctamente List.contains)
covariance implicit-cast (6)
¿Por qué no usar una clase de igualdad?
scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)
scala> class EQ[A](a1:A) { def ===(a2:A) = a1 == a2 }
defined class EQ
scala> implicit def toEQ[A](a1:A) = new EQ(a1)
toEQ: [A](a1: A)EQ[A]
scala> l exists (1===)
res7: Boolean = true
scala> l exists ("1"===)
<console>:14: error: type mismatch;
found : java.lang.String => Boolean
required: Int => Boolean
l exists ("1"===)
^
scala> List("1","2")
res9: List[java.lang.String] = List(1, 2)
scala> res9 exists (1===)
<console>:14: error: type mismatch;
found : Int => Boolean
required: java.lang.String => Boolean
res9 exists (1===)
List("a").contains(5)
Debido a que un Int
no puede estar contenido en una lista de String
, esto debería generar un error en tiempo de compilación , pero no lo hace.
Prueba de manera desperdiciada y silenciosa cada String
contenida en la lista para la igualdad de 5
, lo que nunca puede ser cierto ( "5"
nunca es igual a 5
en Scala).
Esto se ha denominado " el problema ''contiene'' ". Y algunos han implicado que si un sistema de tipos no puede escribir correctamente esa semántica, entonces, ¿por qué pasar por el esfuerzo extra para imponer tipos? Por eso considero que es un problema importante a resolver.
El tipo de parametrización B >: A
de List.contains
cualquier tipo que sea un supertipo del tipo A
(el tipo de los elementos contenidos en la lista).
trait List[+A] {
def contains[B >: A](x: B): Boolean
}
Este tipo de parametrización es necesario porque el +A
declara que la lista es covariant en el tipo A
, por lo que A
no se puede usar en la posición contravariante, es decir, como el tipo de un parámetro de entrada. Las listas covariantes (que deben ser inmutables) son mucho más poderosas para la extensión que las listas invariantes (que pueden ser mutables).
A
es una String
en el ejemplo problemático anterior, pero Int
no es un supertipo de String
, así que, ¿qué pasó? La subsunción implícita en Scala, decidió que Any
es un supertipo mutuo tanto de String
como de Int
.
El creador de Scala, Martin Odersky, suggested que una solución sería limitar el tipo de entrada B
a solo aquellos tipos que tienen un método igual que Any
no tiene.
trait List[+A] {
def contains[B >: A : Eq](x: B): Boolean
}
Pero eso no resuelve el problema, porque dos tipos (donde el tipo de entrada no es un supertipo del tipo de los elementos de la lista) pueden tener un supertipo mutuo que es un subtipo de Any
, es decir, también un subtipo de Eq
. Por lo tanto, se compilaría sin error y la semántica tipeada incorrectamente permanecería.
Deshabilitar la subsunción implícita en todas partes tampoco es una solución ideal , porque la subsunción implícita es la razón por la cual el siguiente ejemplo para la subsunción a Any
funciona. Y no nos gustaría ser forzados a usar conversiones de tipo cuando el sitio de recepción (por ejemplo, pasando como un argumento de función) ha escrito correctamente la semántica para un supertipo mutuo (que podría no ser Any
).
trait List[+A] {
def ::[B >: A](x: B): List[B]
}
val x : List[Any] = List("a", 5) // see[1]
[1] List.apply llama al operador :: .
Entonces mi pregunta es ¿cuál es la mejor solución para este problema?
Mi conclusión tentativa es que la subsunción implícita debe desactivarse en el sitio de definición donde la semántica no se escribe correctamente. Proporcionaré una respuesta que muestra cómo desactivar la subsunción implícita en el sitio de definición de métodos. ¿Hay soluciones alternativas?
Tenga en cuenta que este problema es general y no está aislado a las listas.
ACTUALIZACIÓN : Presenté una solicitud de mejora y comencé un hilo de discusión sobre esto . También he agregado comentarios bajo las respuestas de Kim Stebel y Peter Schmitz que muestran que sus respuestas tienen una funcionalidad errónea. Por lo tanto no hay solución. También en el hilo de discusión mencionado anteriormente, expliqué por qué creo que la respuesta de soc no es correcta.
Creo que no entiendes la solución de Martin, no es B <: Eq
, es B : Eq
, que es un atajo para
def Contains[B >: A](x: B)(implicit ev: Eq[B])
Y la Eq[X]
contendría entonces un método
def areEqual(a: X, b: X): Boolean
Esto no es lo mismo que mover el método equals de Any un poco más abajo en la jerarquía, lo que de hecho no resolvería el problema de tenerlo en Any.
Creo que tengo una solución legítima para al menos parte del problema publicado aquí, es decir, el problema con la List("1").contains(1)
: https://docs.google.com/document/d/1sC42GKY7WvztXzgWPGDqFukZ0smZFmNnQksD_lJzm20/edit
En mi extensión de biblioteca utilizo:
class TypesafeEquals[A](val a: A) {
def =*=(x: A): Boolean = a == x
def =!=(x: A): Boolean = a != x
}
implicit def any2TypesafeEquals[A](a: A) = new TypesafeEquals(a)
class RichSeq[A](val seq: Seq[A]) {
...
def containsSafely(a: A): Boolean = seq exists (a =*=)
...
}
implicit def seq2RichSeq[A](s: Seq[A]) = new RichSeq(s)
Así que evito llamar a los contains
.
Esto suena bien en teoría, pero en mi opinión se desmorona en la vida real.
equals
no se basa en tipos y contains
está construyendo encima de eso.
Es por eso que un código como 1 == BigInt(1)
funciona y devuelve el resultado que la mayoría de la gente esperaría.
En mi opinión, no tiene sentido hacer contains
más estrictos que equals
.
Si los contains
se hicieran más estrictos, el código como List[BigInt](1,2,3) contains 1
dejaría de funcionar por completo.
Por cierto, no creo que "inseguro" o "no escriba seguro" sean los términos correctos aquí.
Los ejemplos usan L
lugar de List
o SeqLike
, porque para que esta solución se aplique a un método preexistente de esas colecciones, se requeriría un cambio en el código de la biblioteca preexistente. Uno de los objetivos es la mejor manera de lograr la igualdad, no el mejor compromiso para interoperar con las bibliotecas actuales (aunque se debe considerar la compatibilidad con versiones anteriores). Además, mi otro objetivo es que esta respuesta es generalmente aplicable para cualquier función de método que desee deshabilitar selectivamente la función de subsunción implícita del compilador de Scala por cualquier motivo, no necesariamente vinculada a la semántica de igualdad.
case class L[+A]( elem: A )
{
def contains[B](x: B)(implicit ev: A <:< B) = elem == x
}
Lo anterior genera un error según lo deseado, asumiendo que la semántica deseada para List.contains
es que la entrada debe ser igual y un supertipo del elemento contenido.
L("a").contains(5)
error: could not find implicit value for parameter ev: <:<[java.lang.String,Int]
L("a").contains(5)
^
El error no se genera cuando no se requiere la subsunción implícita.
scala> L("a").contains(5 : Any)
defined class L
scala> L("a").contains("")
defined class L
Esto desactiva la subsunción implícita (selectivamente en el sitio de definición del método), al exigir que el tipo de parámetro de entrada B
sea el mismo que el tipo de argumento pasado como entrada (es decir, no implícitamente sumable con A
), y luego, por separado, requiere evidencia implícita de que B
es a, o tiene un supertipo implícitamente subsumible de A
].
ACTUALIZACIÓN 03 de mayo de 2012 : el código anterior no está completo, como se muestra a continuación, al desactivar toda subsunción en el sitio de definición de métodos no se obtiene el resultado deseado.
class Super
defined class Super
class Sub extends Super
defined class Sub
L(new Sub).contains(new Super)
defined class L
L(new Super).contains(new Sub)
error: could not find implicit value for parameter ev: <:<[Super,Sub]
L(new Super).contains(new Sub)
^
La única forma de obtener la forma de subsunción deseada es emitir también en el sitio de uso (llamada) del método.
L(new Sub).contains(new Super : Sub)
error: type mismatch;
found : Super
required: Sub
L(new Sub).contains(new Super : Sub)
^
L(new Super).contains(new Sub : Super)
defined class L
Según share , la semántica actual para List.contains
es que la entrada debe ser igual, pero no necesariamente un supertipo del elemento contenido. Esto supone que List.contains
promete que cualquier elemento coincidente solo es igual y no se requiere que sea una copia (subtipo o) de una instancia de la entrada. La interfaz de igualdad universal actual Any.equals : Any => Boolean
está unida, por lo que la igualdad no impone una relación de subtipo. Si esta es la semántica deseada para List.contains
, las relaciones de subtipo no se pueden emplear para optimizar la semántica de tiempo de compilación, por ejemplo, deshabilitar la subsunción implícita, y estamos atascados con las posibles ineficiencias semánticas que degradan el rendimiento en tiempo de ejecución para List.contains
.
Mientras estudiaré y pensaré más sobre la igualdad y el contenido, afaics mi respuesta sigue siendo válida con el propósito general de deshabilitar selectivamente la subsunción implícita en el sitio de definición de métodos.
Mi proceso de pensamiento también está en marcha de manera holística y es el mejor modelo de igualdad.
Actualización : Agregué un comentario debajo de share , así que ahora creo que su punto no es relevante. La igualdad siempre debe basarse en una relación subtipo, que afaics es lo que Martin Odersky propone para la nueva revisión de la igualdad (ver también su suggested de contains
). Cualquier equivalencia polimórfica ad hoc (por ejemplo, BitInt(1) == 1
) se puede manejar con conversiones implícitas. Expliqué en mi comentario a continuación share que sin mi mejora a continuación, las propuestas de Martin contains
tendrían un error semántico, por lo que un supertipo mutuo implícitamente subsumido (distinto de Any
) seleccionará la instancia implícita incorrecta de la Eq
(si existe alguna, no es necesario) error del compilador). Mi solución deshabilita la subsunción implícita para este método, que es la semántica correcta para el argumento de Eq.eq
de Eq.eq
trait Eq[A]
{
def eq(x: A, y: A) = x == y
}
implicit object EqInt extends Eq[Int]
implicit object EqString extends Eq[String]
case class L[+A]( elem: A )
{
def contains[B](x: B)(implicit ev: A <:< B, eq: Eq[B]) = eq.eq(x, elem)
}
L("a").contains("")
Nota Eq.eq
puede ser reemplazado opcionalmente por el implicit object
(no reemplazado porque no hay herencia virtual, ver más abajo).
Tenga en cuenta que, como se desea, L("a").contains(5 : Any)
ya no se compila, porque ya no se usa Any.equals
.
Podemos abreviar.
case class L[+A]( elem: A )
{
def contains[B : Eq](x: B)(implicit ev: A <:< B) = eq.eq(x, elem)
}
Agregar : La x == y
debe ser una llamada de herencia virtual, es decir, x.==
debe declararse override
, porque no hay herencia virtual en la clase de tipos Eq
. El tipo de parámetro A
es invariante (porque A
se usa en la posición contravariante como parámetro de entrada de Eq.eg
). Entonces podemos definir un implicit object
en una interfaz (también conocido como trait
).
Por lo tanto, la anulación de Any.equals
todavía debe verificar si el tipo concreto de la entrada coincide. Esa sobrecarga no puede ser eliminada por el compilador.