generics - instanciar - Scala: Tipos abstractos vs genéricos
instanciar clases scala (3)
Estaba leyendo Un tour de Scala: Tipos abstractos . ¿Cuándo es mejor usar tipos abstractos?
Por ejemplo,
abstract class Buffer {
type T
val element: T
}
más bien que los genéricos, por ejemplo,
abstract class Buffer[T] {
val element: T
}
Puede usar tipos abstractos junto con parámetros de tipo para establecer plantillas personalizadas.
Supongamos que necesita establecer un patrón con tres rasgos conectados:
trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
en la forma en que los argumentos mencionados en los parámetros de tipo son AA, BB, CC, respetuosamente
Puedes venir con algún tipo de código:
trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
lo que no funcionaría de esta manera simple debido a los enlaces de parámetros de tipo. Necesitas que sea covariante para heredar correctamente.
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
Esta muestra se compilaría, pero establece requisitos sólidos en las reglas de variación y no se puede usar en algunas ocasiones
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
def forth(x:B):C
def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
def forth(x:C):A
def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
def forth(x:A):B
def back(x:B):A
}
El compilador se opondrá con un montón de errores de verificación de varianza
En ese caso, puede reunir todos los requisitos de tipo en rasgos adicionales y parametrizar otros rasgos sobre ellos.
//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
type A <: AA[O]
type B <: BB[O]
type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
type A = O#A
type B = O#B
type C = O#C
def left(l:B):C
def right(r:C):B = r.left(this)
def join(l:B, r:C):A
def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
type A = O#A
type B = O#B
type C = O#C
def left(l:C):A
def right(r:A):C = r.left(this)
def join(l:C, r:A):B
def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
type A = O#A
type B = O#B
type C = O#C
def left(l:A):B
def right(r:B):A = r.left(this)
def join(l:A, r:B):C
def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}
Ahora podemos escribir una representación concreta para el patrón descrito, definir los métodos de izquierda y combinación en todas las clases y obtener el derecho y el doble gratis
class ReprO extends OO[ReprO] {
override type A = ReprA
override type B = ReprB
override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
override def left(l:B):C = ReprC(data - l.data)
override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
override def left(l:C):A = ReprA(data - l.data)
override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
override def left(l:A):B = ReprB(data - l.data)
override def join(l:A, r:B):C = ReprC(l.data + r.data)
}
Por lo tanto, tanto los tipos abstractos como los parámetros de tipo se utilizan para crear abstracciones. Ambos tienen punto débil y fuerte. Los tipos abstractos son más específicos y capaces de describir cualquier estructura de tipo, pero son detallados y requieren una especificación explícita. Los parámetros de tipo pueden crear un montón de tipos al instante, pero le da una preocupación adicional sobre la herencia y los límites de tipo.
Se dan sinergia entre sí y se pueden usar en conjunto para crear abstracciones complejas que no se pueden expresar con solo uno de ellos.
Tuve la misma pregunta cuando estaba leyendo sobre Scala.
La ventaja de usar genéricos es que está creando una familia de tipos. Nadie necesitará subclasificar Buffer
solo pueden usar Buffer[Any]
, Buffer[String]
, etc.
Si usa un tipo abstracto, entonces las personas se verán obligadas a crear una subclase. La gente necesitará clases como AnyBuffer
, StringBuffer
, etc.
Necesita decidir cuál es mejor para su necesidad particular.
Usted tiene un buen punto de vista sobre este tema aquí:
El propósito del sistema tipográfico de Scala
Una conversación con Martin Odersky, Parte III
por Bill Venners y Frank Sommers (18 de mayo de 2009)
Actualización (octubre de 2009): lo que sigue a continuación ha sido ilustrado en este nuevo artículo por Bill Venners:
Tipo de resumen Miembros versus parámetros de tipo genérico en Scala (ver resumen al final)
(Aquí está el extracto relevante de la primera entrevista, mayo de 2009, énfasis mío)
Principio general
Siempre ha habido dos nociones de abstracción:
- parametrización y
- miembros abstractos
En Java también tienes ambos, pero depende de lo que estés abstrayendo.
En Java tiene métodos abstractos, pero no puede pasar un método como parámetro.
No tiene campos abstractos, pero puede pasar un valor como parámetro.
Y de manera similar, no tiene miembros de tipo abstracto, pero puede especificar un tipo como parámetro.
Así que en Java también tienes estos tres, pero hay una distinción sobre qué principio de abstracción puedes usar para qué tipo de cosas. Y se podría argumentar que esta distinción es bastante arbitraria.
El camino de Scala
Decidimos tener los mismos principios de construcción para los tres tipos de miembros .
Así que puedes tener campos abstractos así como parámetros de valor.
Puede pasar métodos (o "funciones") como parámetros, o puede abstraerlos.
Puede especificar tipos como parámetros, o puede abstraerlos.
Y lo que obtenemos conceptualmente es que podemos modelar uno en términos del otro. Al menos en principio, podemos expresar todo tipo de parametrización como una forma de abstracción orientada a objetos. Entonces, en cierto sentido, se podría decir que Scala es un lenguaje más ortogonal y completo.
¿Por qué?
Lo que, en particular, los tipos abstractos que compran es un buen tratamiento para estos problemas de covarianza de los que hablamos antes.
Un problema estándar, que ha existido durante mucho tiempo, es el problema de los animales y los alimentos.
El rompecabezas era tener un Animal
clase con un método, eat
, que come algo.
El problema es que si hacemos una subclase de Animal y tenemos una clase como Vaca, entonces solo comerían Hierba y no comida arbitraria. Una vaca no podía comer un pescado, por ejemplo.
Lo que quieres es poder decir que una vaca tiene un método de comer que solo come hierba y no otras cosas.
En realidad, no puedes hacer eso en Java porque resulta que puedes construir situaciones no sólidas, como el problema de asignar una Fruta a una variable de Apple de la que hablé antes.
La respuesta es que agregas un tipo abstracto a la clase Animal .
Usted dice, mi nueva clase de animales tiene un tipo de SuitableFood
, que no sé.
Así que es un tipo abstracto. No das una implementación del tipo. Entonces tienes un método de eat
que solo come eat
SuitableFood
.
Y luego, en la clase Cow
, diría, está bien, tengo una Vaca, que se extiende a la clase Animal
, y para el Cow type SuitableFood equals Grass
.
Así que los tipos abstractos proporcionan esta noción de un tipo en una superclase que no conozco, que luego relleno en subclases con algo que sí sé .
¿Lo mismo con la parametrización?
De hecho se puede. Podrías parametrizar la clase Animal con el tipo de comida que come.
Pero en la práctica, cuando haces eso con muchas cosas diferentes, esto conduce a una explosión de parámetros , y generalmente, lo que es más, dentro de los límites de los parámetros .
En el ECOOP de 1998, Kim Bruce, Phil Wadler y yo tuvimos un artículo en el que demostramos que a medida que aumenta la cantidad de cosas que no sabe, el programa típico crecerá de forma cuadrática .
Así que hay muy buenas razones para no hacer parámetros, pero para tener estos miembros abstractos, porque no te dan esta explosión cuadrática.
pregunta en los comentarios:
¿Crees que el siguiente es un resumen justo?
- Los tipos abstractos se utilizan en las relaciones ''has-a'' o ''uses-a'' (por ejemplo, una
Cow eats Grass
)- donde los genéricos suelen ser "de" relaciones (por ejemplo,
List of Ints
)
No estoy seguro de que la relación sea tan diferente entre usar tipos abstractos o genéricos. Lo que es diferente es:
- como se usan, y
- Cómo se gestionan los límites de los parámetros.
Para entender a qué se refiere Martin cuando se trata de "explosión de parámetros, y generalmente, lo que es más, dentro de los límites de los parámetros ", y su posterior crecimiento cuadrático cuando el tipo abstracto se modela usando genéricos, puede considerar el documento " Abstracción de componentes escalables "escrito por ... Martin Odersky y Matthias Zenger para OOPSLA 2005, a los que se hace referencia en las publicaciones del proyecto Palcom (finalizado en 2007).
Extractos relevantes
Definición
Los miembros de tipo abstracto proporcionan una forma flexible de abstraer sobre tipos de componentes concretos.
Los tipos de resumen pueden ocultar información sobre las partes internas de un componente, similar a su uso en firmas SML . En un marco orientado a objetos donde las clases se pueden extender por herencia, también se pueden usar como un medio flexible de parametrización (a menudo llamado polimorfismo familiar, consulte esta entrada del blog, por ejemplo , y el documento escrito por Eric Ernst ).
(Nota: se ha propuesto el polimorfismo familiar para los lenguajes orientados a objetos como una solución para soportar clases mutuamente recursivas reutilizables pero seguras de tipos.
Una idea clave del polimorfismo familiar es la noción de familias, que se utilizan para agrupar clases recursivas entre sí.
abstracción de tipo acotado
abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}
Aquí, la declaración de tipo de T está limitada por un límite de tipo superior que consta de un nombre de clase Ordenado y un refinamiento
{ type O = T }
.
El límite superior restringe las especializaciones de T en subclases a aquellos subtipos de Ordenados para los cuales el miembro tipoO
deequals T
Debido a esta restricción, se garantiza que el<
método de clase Ordenado sea aplicable a un receptor y un argumento de tipo T.
El ejemplo muestra que el miembro de tipo limitado puede aparecer como parte del límite.
(Es decir, Scala es compatible con el polimorfismo limitado por F )
(Nota, de Peter Canning, William Cook, Walter Hill, trabajo de Walter Olthoff:
Cardelli y Wegner introdujeron la cuantificación acotada como un medio para escribir funciones que operan de manera uniforme en todos los subtipos de un tipo dado.
Definieron un modelo simple de "objetos" y utilizaron la cuantificación acotada para las funciones de verificación de tipo que tienen sentido en todos los objetos que tienen un conjunto específico de "atributos".
Una presentación más realista de lenguajes orientados a objetos permitiría objetos que son elementos de tipos definidos recursivamente .
En este contexto, la cuantificación limitada ya no sirve a su propósito previsto. Es fácil encontrar funciones que tengan sentido en todos los objetos que tienen un conjunto específico de métodos, pero que no se pueden escribir en el sistema Cardelli-Wegner.
Para proporcionar una base para funciones polimórficas mecanografiadas en lenguajes orientados a objetos, introducimos la cuantificación limitada por F)
Dos caras de las mismas monedas.
Hay dos formas principales de abstracción en los lenguajes de programación:
- parametrización y
- miembros abstractos
La primera forma es típica de los lenguajes funcionales, mientras que la segunda forma se usa típicamente en lenguajes orientados a objetos.
Tradicionalmente, Java admite la parametrización de valores y la abstracción de miembros para operaciones. El Java 5.0 más reciente con genéricos también admite la parametrización para tipos.
Los argumentos para incluir genéricos en Scala son dobles:
Primero, la codificación en tipos abstractos no es tan sencilla de hacer a mano. Además de la pérdida de concisión, también existe el problema de conflictos accidentales de nombres entre nombres de tipos abstractos que emulan parámetros de tipo.
En segundo lugar, los tipos genéricos y abstractos suelen cumplir funciones distintas en los programas de Scala.
- Los genéricos se utilizan normalmente cuando se necesita solo una instanciación de tipo , mientras que
- los tipos abstractos se utilizan normalmente cuando es necesario referirse al tipo abstracto del código del cliente .
Este último surge en particular en dos situaciones: - Uno podría querer ocultar la definición exacta de un miembro de tipo del código del cliente, para obtener un tipo de encapsulación conocido de los sistemas de módulos de estilo SML.
- O uno podría querer anular el tipo covariante en las subclases para obtener polimorfismo familiar.
En un sistema con polimorfismo acotado, reescribir el tipo abstracto en genéricos podría implicar una expansión cuadrática de los límites de tipo .
Actualización octubre 2009
Miembros de tipo abstracto versus parámetros de tipo genérico en Scala (Bill Venners)
(énfasis mío)
Mi observación hasta ahora acerca de los miembros de tipo abstracto es que son principalmente una mejor opción que los parámetros de tipo genérico cuando:
- quiere permitir que las personas mezclen definiciones de esos tipos a través de rasgos .
- cree que la mención explícita del tipo de nombre de miembro cuando se está definiendo ayudará a la legibilidad del código .
Ejemplo:
Si desea pasar tres objetos de aparatos diferentes a las pruebas, podrá hacerlo, pero deberá especificar tres tipos, uno para cada parámetro. Por lo tanto, si hubiera tomado el enfoque de parámetros de tipo, las clases de su suite podrían haber terminado con este aspecto:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
Mientras que con el tipo de enfoque de miembro se verá así:
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
Otra diferencia menor entre los miembros de tipo abstracto y los parámetros de tipo genérico es que cuando se especifica un parámetro de tipo genérico, los lectores del código no ven el nombre del parámetro de tipo. Así fue que alguien vio esta línea de código:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
No sabrían cuál era el nombre del parámetro de tipo especificado como StringBuilder sin buscarlo. Mientras que el nombre del parámetro de tipo está justo ahí en el código del método de miembro de tipo abstracto:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
En este último caso, los lectores del código podrían ver que
StringBuilder
es el tipo de "parámetro de accesorio".
Aún tendrían que averiguar qué significaba el "parámetro de accesorio", pero al menos podrían obtener el nombre del tipo sin mirar la documentación.