La interfaz de Java y la clase de tipos de Haskell: ¿diferencias y similitudes?

Mientras estoy aprendiendo Haskell, noté su tipo de clase , que se supone que es un gran invento que se originó en Haskell.

Sin embargo, en la página de Wikipedia sobre la clase de tipo :

El progtwigdor define una clase de tipo especificando un conjunto de funciones o nombres de constantes, junto con sus respectivos tipos, que deben existir para cada tipo que pertenezca a la clase.

Lo que me parece bastante cercano a la Interfaz de Java (citando la página de Interfaz de Wikipedia (Java) ):

Una interfaz en el lenguaje de progtwigción Java es un tipo abstracto que se utiliza para especificar una interfaz (en el sentido genérico del término) que las clases deben implementar.

Estos dos parecen bastante similares: la clase de tipo limita el comportamiento de un tipo, mientras que la interfaz limita el comportamiento de una clase.

Me pregunto cuáles son las diferencias y similitudes entre la clase de tipo en Haskell y la interfaz en Java, o tal vez son fundamentalmente diferentes.

EDITAR: Me di cuenta incluso haskell.org admite que son similares . Si son tan similares (¿o lo son?), ¿Por qué la clase de tipos se trata con tanta exageración?

MÁS EDITAR: ¡ Guau, tantas buenas respuestas! Supongo que tendré que dejar que la comunidad decida cuál es la mejor. Sin embargo, al leer las respuestas, todos parecen decir simplemente que “hay muchas cosas que la clase de tipos puede hacer mientras que la interfaz no puede o tiene que hacer frente a los generics” . No puedo evitar preguntarme: ¿hay algo que puedan hacer las interfaces mientras que las clases de tipos no pueden? Además, noté que Wikipedia afirma que typeclass se inventó originalmente en el artículo de 1989 * “Cómo hacer que el polymorphism ad hoc sea menos ad hoc”, mientras que Haskell todavía está en su cuna, mientras que el proyecto Java se inició en 1991 y se lanzó por primera vez en 1995. Entonces, ¿ tal vez en lugar de que typeclass sea similar a las interfaces, es al revés, que las interfaces estén influenciadas por typeclass? ¿Hay documentos / documentos que apoyen o refuten esto? Gracias por todas las respuestas, ¡todas son muy esclarecedoras!

¡Gracias por todas las entradas!

Diría que una interfaz es algo así como una clase de tipo SomeInterface t donde todos los valores tienen el tipo t -> whatever (donde whatever que no contenga t ). Esto se debe a que con el tipo de relación de herencia en Java y en lenguajes similares, el método llamado depende del tipo de objeto al que se les llama y nada más.

Eso significa que es realmente difícil hacer cosas como add :: t -> t -> t con una interfaz, donde es polimórfica en más de un parámetro, porque no hay manera de que la interfaz especifique que el tipo de argumento y el tipo de retorno de el método es el mismo tipo que el tipo del objeto al que se llama (es decir, el tipo “self”). Con los generics, hay algunas formas de simular esto haciendo una interfaz con un parámetro genérico que se espera que sea del mismo tipo que el objeto en sí, como lo hace Comparable , donde se espera que uses los Foo implements Comparable para que el tipo compareTo(T otherobject) tenga el tipo t -> t -> Ordering . Pero eso aún requiere que el progtwigdor siga esta regla, y también causa dolores de cabeza cuando la gente quiere hacer una función que utiliza esta interfaz, tienen que tener parámetros de tipo generics recursivos.

Además, no tendrás cosas como empty :: t porque no estás llamando a una función aquí, por lo que no es un método.

Lo que es similar entre las interfaces y las clases de tipo es que nombran y describen un conjunto de operaciones relacionadas. Las operaciones en sí se describen a través de sus nombres, entradas y salidas. Asimismo, puede haber muchas implementaciones de estas operaciones que probablemente difieran en su implementación.

Con eso fuera del camino, aquí hay algunas diferencias notables:

  • Los métodos de interfaz siempre están asociados con una instancia de objeto. En otras palabras, siempre hay un parámetro ‘esto’ implícito que es el objeto sobre el que se llama el método. Todas las entradas a una función de clase de tipo son explícitas.
  • Una implementación de interfaz debe definirse como parte de la clase que implementa la interfaz. A la inversa, una clase de tipo ‘instancia’ se puede definir completamente separada de su tipo asociado … incluso en otro módulo.
  • Una clase de tipo le permite definir una implementación ‘predeterminada’ para cualquiera de las operaciones definidas. Las interfaces son estrictamente especificaciones de tipo, sin implementación.

En general, creo que es justo decir que las clases de tipos son más potentes y flexibles que las interfaces. ¿Cómo definiría una interfaz para convertir una cadena a algún valor o instancia del tipo de implementación? Ciertamente no es imposible, pero el resultado no sería intuitivo o elegante. ¿Alguna vez ha deseado que fuera posible implementar una interfaz para un tipo en alguna biblioteca comstackda? Estos son fáciles de lograr con las clases de tipo.

Las clases de tipo se crearon como una forma estructurada de express “polymorphism ad-hoc”, que es básicamente el término técnico para funciones sobrecargadas . Una definición de clase de tipo se ve algo como esto:

 class Foobar a where foo :: a -> a -> Bool bar :: String -> a 

Lo que esto significa es que, cuando utilizas la función foo para algunos argumentos de un tipo que pertenece a la clase Foobar , busca una implementación de foo específica para ese tipo, y la usa. Esto es muy similar a la situación con la sobrecarga de operadores en lenguajes como C, excepto que es más flexible y generalizada.

Las interfaces tienen un propósito similar en los idiomas OO, pero el concepto subyacente es algo diferente; Los lenguajes OO vienen con una noción incorporada de jerarquías de tipos que Haskell simplemente no tiene, lo que complica las cosas de alguna manera porque las interfaces pueden involucrar tanto la sobrecarga mediante el subtipo (es decir, llamar a los métodos en casos apropiados, los subtipos que implementan interfaces a sus supertipos) y por despacho de tipo plano (ya que dos clases que implementan una interfaz pueden no tener una superclase común que también la implemente). Dada la gran complejidad adicional introducida por la subtipificación, sugiero que es más útil pensar en las clases de tipos como una versión mejorada de las funciones sobrecargadas en un idioma que no sea OO.

También es de destacar que las clases de tipo tienen medios de envío mucho más flexibles: las interfaces generalmente se aplican solo a la clase única que lo implementa, mientras que las clases de tipo se definen para un tipo , que puede aparecer en cualquier lugar en la firma de las funciones de la clase. El equivalente de esto en las interfaces OO sería permitir que la interfaz defina formas de pasar un objeto de esa clase a otras clases, definir métodos estáticos y constructores que seleccionen una implementación en función del tipo de retorno requerido en el contexto de llamada, definir métodos que tome argumentos del mismo tipo que la clase que implementa la interfaz y varias otras cosas que realmente no se traducen en absoluto.

En resumen: sirven para propósitos similares, pero su forma de trabajar es algo diferente, y las clases de tipos son significativamente más expresivas y, en algunos casos, más fáciles de usar debido a que trabajan con tipos fijos en lugar de partes de una jerarquía de herencia.

He leído las respuestas anteriores. Siento que puedo responder un poco más claro:

Una “clase de tipo” de Haskell y una “interfaz” de Java / C # o un “rasgo” de Scala son básicamente análogas. No hay una distinción conceptual entre ellos, pero hay diferencias de implementación:

  • Las clases de tipo Haskell se implementan con “instancias” que son independientes de la definición del tipo de datos. En C # / Java / Scala, las interfaces / rasgos deben implementarse en la definición de clase.
  • Las clases de tipo Haskell le permiten devolver este tipo o tipo de uno mismo. Los rasgos de Scala también lo hacen (este tipo). Tenga en cuenta que los “tipos automáticos” en Scala son una característica completamente no relacionada. Java / C # requiere una solución desordenada con generics para aproximar este comportamiento.
  • Las clases de tipo Haskell le permiten definir funciones (incluidas las constantes) sin un parámetro de tipo “this” de entrada. Las interfaces Java / C # y los rasgos de Scala requieren un parámetro de entrada “this” en todas las funciones.
  • Las clases de tipos de Haskell le permiten definir implementaciones predeterminadas para funciones. Lo mismo ocurre con los rasgos de Scala y las interfaces Java 8+. C # puede aproximar algo como esto con métodos de extensiones.

Mire la charla de Phillip Wadler Faith, Evolution, and Programming Languages . Wadler trabajó en Haskell y fue un importante colaborador de Java Generics.

En las mentes maestras de la progtwigción , hay una entrevista sobre Haskell con Phil Wadler, el inventor de las clases de tipos, que explican las similitudes entre las interfaces en Java y las clases de tipos en Haskell:

Un método Java como:

  public static > T min (T x, T y) { if (x.compare(y) < 0) return x; else return y; } 

Es muy similar al método de Haskell:

  min :: Ord a => a -> a -> a min xy = if x < y then x else y 

Por lo tanto, las clases de tipo están relacionadas con las interfaces, pero la correspondencia real sería un método estático parametrizado con un tipo como el anterior.

Lea Extensión de software e integración con clases de tipos donde se dan ejemplos de cómo las clases de tipos pueden resolver una serie de problemas que las interfaces no pueden.

Los ejemplos enumerados en el documento son:

  • el problema de la expresión,
  • el problema de integración del marco,
  • El problema de la extensibilidad independiente.
  • La tiranía de la descomposición dominante, la dispersión y el enredo.

No puedo hablar con el nivel de “bombo”, si me parece que está bien. Pero sí, las clases de tipos son similares en muchos sentidos. Una diferencia que se me ocurre es que Haskell puede proporcionar comportamiento para algunas de las operaciones de la clase de tipos:

 class Eq a where (==), (/=) :: a -> a -> Bool x /= y = not (x == y) x == y = not (x /= y) 

que muestra que hay dos operaciones, igual (==) y no igual (/=) , para cosas que son instancias de la clase de tipo Eq . Pero la operación no igual se define en términos de iguales (de modo que solo tendría que proporcionar uno) y viceversa.

Entonces, en Java probablemente no legal, sería algo así como:

 interface Equal { bool isEqual(T other) { return !isNotEqual(other); } bool isNotEqual(T other) { return !isEqual(other); } } 

y la forma en que funcionaría es que solo tendría que proporcionar uno de esos métodos para implementar la interfaz. Entonces, diría que la capacidad de proporcionar una especie de implementación parcial del comportamiento que desea en el nivel de interfaz es una diferencia.

Son similares (léase: tienen un uso similar), y probablemente se implementen de manera similar: las funciones polimórficas en Haskell toman bajo el capó un ‘vtable’ que enumera las funciones asociadas con la clase de tipo.

Esta tabla a menudo se puede deducir en tiempo de comstackción. Esto es probablemente menos cierto en Java.

Pero esta es una tabla de funciones , no de métodos . Los métodos están vinculados a un objeto, las clases de tipos Haskell no lo son.

Véalos como los generics de Java.

Como dice Daniel, las implementaciones de interfaz se definen por separado de las declaraciones de datos. Y como otros han señalado, existe una manera directa de definir operaciones que usan el mismo tipo gratuito en más de un lugar. Así que es fácil de definir Num como una clase de tipos. Por lo tanto, en Haskell obtenemos los beneficios sintácticos de la sobrecarga del operador sin tener en realidad operadores mágicamente sobrecargados, solo clases de tipos estándar.

Otra diferencia es que puede usar métodos basados ​​en un tipo, ¡incluso cuando todavía no tiene un valor concreto de ese tipo!

Por ejemplo, read :: Read a => String -> a . Entonces, si tiene suficiente información de otro tipo acerca de cómo usará el resultado de una “lectura”, puede dejar que el comstackdor averigüe qué diccionario usar para usted.

También puede hacer cosas como instance (Read a) => Read [a] where... que le permite definir una instancia de lectura para cualquier lista de elementos legibles. No creo que eso sea bastante posible en Java.

Y todo esto es solo las clases de tipo estándar de un solo parámetro sin ningún engaño. Una vez que introducimos clases de tipos de parámetros múltiples, entonces se abre un nuevo mundo de posibilidades, y más aún con las dependencias funcionales y las familias de tipos, que le permiten integrar mucha más información y cálculo en el sistema de tipos.