Tienes 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:
Miembros de tipo abstracto 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 tiene ambos, pero depende de lo que esté 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.
Entonces, en Java también tiene los tres, pero hay una distinción sobre qué principio de abstracción puede usar para qué tipo de cosas. Y podría argumentar que esta distinción es bastante arbitraria.
El camino Scala
Decidimos tener los mismos principios de construcción para los tres tipos de miembros .
Por lo tanto, puede tener campos abstractos, así como parámetros de valor.
Puede pasar métodos (o "funciones") como parámetros, o puede hacer un resumen sobre ellos.
Puede especificar tipos como parámetros, o puede hacer un resumen sobre ellos.
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 te compran es un buen tratamiento para estos problemas de covarianza de los que hablamos anteriormente.
Un problema estándar, que ha existido durante mucho tiempo, es el problema de los animales y los alimentos.
El enigma era tener una clase Animal
con un método eat
que comiera algo.
El problema es que si subclasificamos Animal y tenemos una clase como Vaca, entonces comerían solo 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 puede hacer eso en Java porque resulta que puede construir situaciones poco sólidas, como el problema de asignar una fruta a una variable de Apple de la que hablé anteriormente.
La respuesta es que agrega un tipo abstracto a la clase Animal .
Dices, mi nueva clase Animal tiene un tipo de SuitableFood
, que no sé.
Entonces es un tipo abstracto. No das una implementación del tipo. Entonces tienes un eat
método que solo come SuitableFood
.
Y luego en la Cow
clase diría, OK, tengo una vaca, que extiende la clase Animal
, y para Cow type SuitableFood equals Grass
.
Entonces, los tipos abstractos proporcionan esta noción de un tipo en una superclase que no sé, que luego rellenaré más adelante en las subclases con algo que sí sé .
Lo mismo con la parametrización?
De hecho puedes. Puede parametrizar la clase Animal con el tipo de comida que come.
Pero en la práctica, cuando haces eso con muchas cosas diferentes, se produce una explosión de parámetros y, por lo general, 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 teníamos un documento donde mostramos que a medida que aumenta la cantidad de cosas que no sabe, el programa típico crecerá de forma cuadrática .
Entonces, hay muy buenas razones para no hacer parámetros, sino para tener estos miembros abstractos, porque no te dan esta explosión cuadrática.
thatismatt 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, a
Cow eats Grass
)
- donde los genéricos son generalmente relaciones 'de' (p
List of Ints
. ej. )
No estoy seguro de que la relación sea tan diferente entre usar tipos abstractos o genéricos. Lo que es diferente es:
- cómo se usan, y
- cómo se gestionan los límites de los parámetros.
Para comprender de qué está hablando Martin cuando se trata de "explosión de parámetros y, por lo general, lo que es más, dentro de los límites de los parámetros ", y su posterior crecimiento cuadrático cuando los tipos abstractos se modelan usando genéricos, puede considerar el artículo " Abstracción de componentes escalables "escrito por ... Martin Odersky y Matthias Zenger para OOPSLA 2005, referenciado en las publicaciones del proyecto Palcom (terminado en 2007).
Extractos relevantes
Definición
Los miembros de tipo abstracto proporcionan una forma flexible de abstraer sobre tipos concretos de componentes.
Los tipos abstractos pueden ocultar información sobre componentes internos 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 artículo escrito por Eric Ernst ).
(Nota: el polimorfismo familiar se ha propuesto para los lenguajes orientados a objetos como una solución para apoyar clases recursivas mutuamente seguras y reutilizables.
Una idea clave del polimorfismo familiar es la noción de familias, que se utilizan para agrupar clases recursivas mutuas)
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 consiste en un nombre de clase Ordenado y un refinamiento { type O = T }
.
El límite superior restringe las especializaciones de T en las subclases a los subtipos de Ordenado para los que el miembro O
de tipo equals T
.
Debido a esta restricción, <
se garantiza que el método de la clase Ordenada sea aplicable a un receptor y un argumento de tipo T.
El ejemplo muestra que el miembro de tipo acotado puede aparecer como parte del límite.
(es decir, Scala admite polimorfismo limitado por F )
(Nota, de Peter Canning, William Cook, Walter Hill, documento de Walter Olthoff:
Cardelli y Wegner introdujeron la cuantificación limitada como un medio para escribir funciones que operan de manera uniforme en todos los subtipos de un tipo dado.
Definieron un modelo simple de "objeto" y usó la cuantificación acotada para verificar las funciones que tienen sentido en todos los objetos que tienen un conjunto específico de "atributos".
Una presentación más realista de los lenguajes orientados a objetos permitiría objetos que son elementos de tipos definidos recursivamente .
En este contexto, acotado la cuantificación ya no cumple 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 las funciones polimórficas escritas en lenguajes orientados a objetos, presentamos 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 para 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 admite la parametrización también para tipos.
Los argumentos para incluir genéricos en Scala son dobles:
Primero, la codificación en tipos abstractos no es tan fácil de hacer a mano. Además de la pérdida de concisión, también existe el problema de conflictos de nombres accidentales entre nombres de tipos abstractos que emulan parámetros de tipo.
En segundo lugar, los tipos genéricos y abstractos generalmente cumplen roles distintos en los programas Scala.
- Los genéricos generalmente se usan cuando uno solo necesita una instanciación de tipo , mientras que
- Los tipos abstractos se usan típicamente cuando uno necesita referirse al tipo abstracto del código del cliente .
Esto último surge en particular en dos situaciones:
- Es posible que desee ocultar la definición exacta de un miembro de tipo del código del cliente, para obtener un tipo de encapsulación conocida de los sistemas de módulos de estilo SML.
- O uno puede anular el tipo covariantemente en 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 de octubre de 2009
Miembros de tipo abstracto versus parámetros de tipo genérico en Scala (Bill Venners)
(énfasis mío)
Mi observación hasta ahora sobre los miembros de tipo abstracto es que son principalmente una mejor opción que los parámetros de tipo genérico cuando:
- desea que las personas mezclen definiciones de esos tipos a través de rasgos .
- cree que la mención explícita del nombre del miembro tipo cuando se está definiendo ayudará a la legibilidad del código .
Ejemplo:
Si desea pasar tres objetos fijos diferentes a las pruebas, podrá hacerlo, pero deberá especificar tres tipos, uno para cada parámetro. Por lo tanto, si hubiera adoptado el enfoque de parámetro de tipo, sus clases de suite podrían haber terminado luciendo así:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
Mientras que con el enfoque de miembro tipo 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. Por lo tanto, alguien podría ver 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á ahí en el código en el enfoque de miembro de tipo abstracto:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
En el último caso, los lectores del código podrían ver que StringBuilder
es el tipo de "parámetro de dispositivo".
Todavía tendrían que averiguar qué significaba "parámetro de dispositivo", pero al menos podrían obtener el nombre del tipo sin consultar la documentación.