Quizás sea primero útil distinguir entre un tipo y una clase y luego sumergirse en la diferencia entre subtipo y subclasificación.
Para el resto de esta respuesta, voy a suponer que los tipos en discusión son tipos estáticos (ya que el subtipo generalmente aparece en un contexto estático).
Voy a desarrollar un pseudocódigo de juguete para ayudar a ilustrar la diferencia entre un tipo y una clase porque la mayoría de los idiomas los combinan al menos en parte (por una buena razón que mencionaré brevemente).
Comencemos con un tipo. Un tipo es una etiqueta para una expresión en su código. El valor de esta etiqueta y si es consistente (para algún tipo de definición específica de sistema de consistente) con el valor de todas las otras etiquetas puede ser determinado por un programa externo (un typechecker) sin ejecutar su programa. Eso es lo que hace que estas etiquetas sean especiales y merezcan su propio nombre.
En nuestro lenguaje de juguetes, podríamos permitir la creación de etiquetas como esta.
declare type Int
declare type String
Entonces podríamos etiquetar varios valores como de este tipo.
0 is of type Int
1 is of type Int
-1 is of type Int
...
"" is of type String
"a" is of type String
"b" is of type String
...
Con estas declaraciones, nuestro typechecker ahora puede rechazar declaraciones como
0 is of type String
Si uno de los requisitos de nuestro sistema de tipos es que cada expresión tiene un tipo único.
Dejemos de lado por ahora lo torpe que es esto y cómo va a tener problemas para asignar un número infinito de tipos de expresiones. Podemos volver a eso más tarde.
Una clase, por otro lado, es una colección de métodos y campos que se agrupan (potencialmente con modificadores de acceso como privado o público).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
Una instancia de esta clase tiene la capacidad de crear o usar definiciones preexistentes de estos métodos y campos.
Podríamos elegir asociar una clase con un tipo de modo que cada instancia de una clase se etiquete automáticamente con ese tipo.
associate StringClass with String
Pero no todos los tipos necesitan tener una clase asociada.
# Hmm... Doesn't look like there's a class for Int
También es concebible que en nuestro lenguaje de juguete no todas las clases tengan un tipo, especialmente si no todas nuestras expresiones tienen tipos. Es un poco más complicado (pero no imposible) imaginar cómo se verían las reglas de coherencia del sistema de tipos si algunas expresiones tuvieran tipos y otras no.
Además, en nuestro lenguaje de juguete, estas asociaciones no tienen que ser únicas. Podríamos asociar dos clases con el mismo tipo.
associate MyCustomStringClass with String
Ahora tenga en cuenta que no hay ningún requisito para que nuestro typechecker rastree el valor de una expresión (y en la mayoría de los casos no lo hará o es imposible hacerlo). Todo lo que sabe son las etiquetas que le has dicho. Como recordatorio anterior, el comprobador de tipos solo podía rechazar la declaración 0 is of type String
debido a nuestra regla de tipo creada artificialmente de que las expresiones deben tener tipos únicos y ya habíamos etiquetado la expresión como 0
algo diferente. No tenía ningún conocimiento especial del valor de 0
.
Entonces, ¿qué pasa con el subtipo? Subtipo de pozo es un nombre para una regla común en la verificación de tipos que relaja las otras reglas que pueda tener. Es decir, si en A is subtype of B
todas partes su typechecker exige una etiqueta B
, también aceptará un A
.
Por ejemplo, podríamos hacer lo siguiente para nuestros números en lugar de lo que teníamos anteriormente.
declare type NaturalNum
declare type Int
NaturalNum is subtype of Int
0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...
La subclasificación es una forma abreviada de declarar una nueva clase que le permite reutilizar métodos y campos previamente declarados.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
No tenemos que asociar instancias de ExtendedStringClass
con String
como lo hicimos StringClass
ya que, después de todo, es una clase completamente nueva, simplemente no teníamos que escribir tanto. Esto nos permitiría dar ExtendedStringClass
un tipo que es incompatible String
desde el punto de vista del typechecker.
Del mismo modo, podríamos haber decidido hacer una clase completamente nueva NewClass
y hacer
associate NewClass with String
Ahora cada instancia de StringClass
puede ser sustituida NewClass
por el punto de vista del typechecker.
Entonces, en teoría, el subtipo y la subclasificación son cosas completamente diferentes. Pero ningún lenguaje que conozca que tenga tipos y clases realmente hace las cosas de esta manera. Comencemos por reducir nuestro lenguaje y expliquemos los fundamentos de algunas de nuestras decisiones.
En primer lugar, a pesar de que, en teoría, las clases completamente diferentes podrían recibir el mismo tipo o una clase podría recibir el mismo tipo que los valores que no son instancias de ninguna clase, esto obstaculiza gravemente la utilidad del comprobador de tipos. El typechecker es efectivamente despojado de la capacidad de verificar si el método o campo que está llamando dentro de una expresión realmente existe en ese valor, lo que probablemente sea una comprobación que le gustaría si tiene la molestia de jugar junto con un máquina de escribir Después de todo, quién sabe cuál es el valor realmente debajo de esa String
etiqueta; ¡podría ser algo que no tiene, por ejemplo, un concatenate
método en absoluto!
Bien, entonces estipulemos que cada clase genera automáticamente un nuevo tipo del mismo nombre que esa clase y associate
las instancias de ese tipo. Eso nos permite deshacernos de associate
los diferentes nombres entre StringClass
y String
.
Por la misma razón, probablemente queremos establecer automáticamente una relación de subtipo entre los tipos de dos clases donde una es una subclase de otra. Después de toda la subclase se garantiza que tiene todos los métodos y campos que tiene la clase padre, pero lo contrario no es cierto. Por lo tanto, aunque la subclase puede pasar en cualquier momento que necesite un tipo de la clase principal, el tipo de la clase principal debe rechazarse si necesita el tipo de la subclase.
Si combina esto con la estipulación de que todos los valores definidos por el usuario deben ser instancias de una clase, entonces puede is subclass of
hacer doble trabajo y deshacerse de él is subtype of
.
Y esto nos lleva a las características que comparten la mayoría de los lenguajes OO de tipo estático populares. Hay un conjunto de tipos "primitivos" (por ejemplo int
, float
etc.) que no están asociados con ninguna clase y no están definidos por el usuario. Luego tiene todas las clases definidas por el usuario que automáticamente tienen tipos del mismo nombre e identifican subclases con subtipos.
La nota final que haré es en torno a la complejidad de declarar tipos por separado de los valores. La mayoría de los idiomas combinan la creación de los dos, de modo que una declaración de tipo también es una declaración para generar valores completamente nuevos que se etiquetan automáticamente con ese tipo. Por ejemplo, una declaración de clase generalmente crea el tipo, así como una forma de instanciar valores de ese tipo. Esto elimina parte de la fragilidad y, en presencia de constructores, también le permite crear etiquetas de infinitos valores con un tipo de un solo trazo.