oop - ¿Por qué evitar la subtipificación?
scala programming-languages (8)
Centrándose en la subtipificación, ignorando los problemas relacionados con las clases, la herencia, OOP, etc. Tenemos la idea de que la subtipificación representa una relación isa entre tipos. Por ejemplo, los tipos A y B tienen diferentes operaciones, pero si A isa B podemos usar cualquiera de las operaciones de B en un A.
OTOH, usando otra relación tradicional, si C hasa B, entonces podemos reutilizar cualquiera de las operaciones de B en C. Normalmente los lenguajes le permiten escribir uno con una sintaxis más agradable, a.opOnB en lugar de a.super.opOnB como lo sería en el caja de composición, cbopOnB
El problema es que en muchos casos hay más de una forma de relacionar dos tipos. Por ejemplo Real puede integrarse en Complejos asumiendo 0 en la parte imaginaria, pero el Complejo puede incrustarse en Real ignorando la parte imaginaria, por lo que ambos pueden verse como subtipos del otro y las fuerzas de subtipificación se pueden ver como preferidas. Además, hay más relaciones posibles (por ejemplo, ver Complejo como real usando el componente theta de la representación polar).
En la terminología formal generalmente decimos morfismo a tales relaciones entre tipos y hay tipos especiales de morfismos para relaciones con diferentes propiedades (por ejemplo, isomorfismo, homomorfismo).
En un lenguaje con subtipificación generalmente hay mucho más azúcar en las relaciones y, dado que hay muchas incrustaciones posibles, tendemos a ver una fricción innecesaria cuando utilizamos la relación no preferida. Si traemos herencia, clases y OOP a la mezcla, el problema se vuelve mucho más visible y desordenado.
He visto a muchas personas en la comunidad de Scala aconsejar sobre cómo evitar la subtipificación "como una plaga". ¿Cuáles son las diversas razones contra el uso de subtipado? ¿Cuáles son las alternativas?
Creo que el contexto general es que el lenguaje virtual sea lo más "puro" posible (es decir, que use la mayor cantidad posible de funciones puras ), y proviene de la comparación con Haskell.
De " Ruminations de un programador "
Scala, al ser un lenguaje OO-FP híbrido, tiene que encargarse de cuestiones como la subtipificación (que Haskell no tiene).
Como se menciona en esta respuesta PSE :
no hay forma de restringir un subtipo para que no pueda hacer más que el tipo desde el que hereda.
Por ejemplo, si la clase base es inmutable y define un método purofoo(...)
, las clases derivadas no pueden ser mutables o anulanfoo()
con una función que no es pura
Pero la recomendación real sería utilizar la mejor solución adaptada al programa que está desarrollando actualmente.
Creo que muchos programadores de Scala son antiguos programadores de Java. Se utilizan para pensar en términos de subtipos orientados a objetos y deberían ser capaces de encontrar fácilmente una solución tipo OO para la mayoría de los problemas. Pero la programación funcional es un nuevo paradigma por descubrir, por lo que las personas piden un tipo diferente de soluciones.
Los tipos determinan la granularidad de la composición, es decir, de la extensibilidad.
Por ejemplo, una interfaz, por ejemplo, Comparable, que combina (por lo tanto, combina) igualdad y operadores relacionales. Por lo tanto, es imposible componer solo en una de las interfaces relacionales o de igualdad.
En general, el principio de sustitución de la herencia es indecidible. La paradoja de Russell implica que cualquier conjunto que sea extensible (es decir, no enumere el tipo de cada posible miembro o subtipo), puede incluirse a sí mismo, es decir, es un subtipo de sí mismo. Pero para identificar (decidir) qué es un subtipo y no en sí mismo, los invariantes de sí mismo deben estar completamente enumerados, por lo tanto, ya no son extensibles. Esta es la paradoja de que la extensibilidad subtipificada hace que la herencia sea indecidible. Esta paradoja debe existir; de lo contrario, el conocimiento sería estático y, por lo tanto , la formación del conocimiento no existiría .
La composición de funciones es la sustitución surjective de la subtipificación, porque la entrada de una función puede sustituirse por su salida, es decir, en cualquier lugar donde se espera el tipo de salida, el tipo de entrada puede sustituirse, envolviéndolo en la llamada a la función. Pero la composición no hace que el bijectivo contraiga subtipos: al acceder a la interfaz de la salida de una función, no accede a la instancia de entrada de la función.
Por lo tanto, la composición no tiene que mantener las invariantes futuras (es decir, ilimitadas) y, por lo tanto, puede ser tanto extensible como decidible. La subtipificación puede ser MUCHO más poderosa cuando sea demostrable, ya que mantiene este contrato biyectivo, por ejemplo, una función que ordena una lista inmutable del supertipo, puede operar en la lista inmutable del subtipo.
Así que la conclusión es enumerar todas las invariantes de cada tipo (es decir, de sus interfaces), hacer que estos tipos sean ortogonales (maximizar la granularidad de la composición) y luego usar la composición de funciones para lograr la extensión donde esos invariantes no serían ortogonales. Por lo tanto, un subtipo es apropiado solo cuando modela de manera demostrable las invariantes de la interfaz del supertipo, y la (s) interfaz (es) adicional (es) del subtipo son probablemente ortogonales a las invariantes de la interfaz del supertipo. Por lo tanto, las invariantes de las interfaces deben ser ortogonales.
La teoría de categorías proporciona reglas para el modelo de las invariantes de cada subtipo, es decir, de Functor, Aplicativo y Monad, que conservan la composición de la función en tipos levantados , es decir, consulte el ejemplo mencionado anteriormente del poder del subtipado para las listas.
Mi respuesta no responde por qué se evita, pero trata de dar otra pista de por qué se puede evitar.
Usando "clases de tipo" puede agregar una abstracción sobre tipos / clases existentes sin modificarlos. La herencia se usa para expresar que algunas clases son especializaciones de una clase más abstracta. Pero con las clases de tipos puede tomar cualquier clase existente y expresar que todas comparten una propiedad común, por ejemplo, son Comparable
. Y mientras no te preocupes de que sean Comparable
, ni siquiera lo notas. Las clases no heredan ningún método de algún tipo Comparable
siempre que no los uses. Es un poco como programar en lenguajes dinámicos.
Lee adicionalmente:
http://blog.tmorris.net/the-power-of-type-classes-with-scala-implicit-defs/
http://debasishg.blogspot.com/2010/07/refactoring-into-scala-type-classes.html
No conozco a Scala, pero creo que el mantra "prefiere la composición sobre la herencia" se aplica a Scala exactamente de la misma manera que a cualquier otro lenguaje de programación OO (y el subtipado a menudo se usa con el mismo significado que "herencia"). aquí
¿Prefiere la composición sobre la herencia?
encontrarás más información.
Una razón es que equals () es muy difícil de obtener cuando se trata de subtipificación. Consulte Cómo escribir un método de igualdad en Java . Específicamente "Obstáculo # 4: No se puede definir equals como una relación de equivalencia". En esencia: para obtener la igualdad correcta bajo sub-tipado, necesita un doble despacho.
This es el mejor documento que he encontrado sobre el tema. Una cita motivadora del periódico -
Argumentamos que, si bien algunos de los aspectos más simples de los lenguajes orientados a objetos son compatibles con ML, agregar un sistema de objetos basado en clases completamente desarrollado a ML conduce a un sistema de tipo excesivamente complejo y relativamente poca ganancia expresiva