Progtwigción orientada a objetos en Haskell

Estoy tratando de comprender la progtwigción de estilo orientado a objetos en Haskell, sabiendo que las cosas van a ser un poco diferentes debido a la falta de mutabilidad. He jugado con clases de tipo, pero mi comprensión de ellas está limitada a ellas como interfaces. Así que he codificado un ejemplo de C ++, que es el diamante estándar con una base pura y herencia virtual. Bat hereda Flying y Mammal , y Flying y Mammal heredan Animal .

 #include  class Animal { public: virtual std::string transport() const = 0; virtual std::string type() const = 0; std::string describe() const; }; std::string Animal::describe() const { return "I am a " + this->transport() + " " + this->type(); } class Flying : virtual public Animal { public: virtual std::string transport() const; }; std::string Flying::transport() const { return "Flying"; } class Mammal : virtual public Animal { public: virtual std::string type() const; }; std::string Mammal::type() const { return "Mammal"; } class Bat : public Flying, public Mammal {}; int main() { Bat b; std::cout << b.describe() << std::endl; return 0; } 

Básicamente me interesa cómo traducir dicha estructura en Haskell, básicamente eso me permitiría tener una lista de Animal s, como si pudiera tener una serie de punteros (inteligentes) para Animal s en C ++.

Simplemente no quieres hacer eso, ni siquiera comiences. OO seguro tiene sus méritos, pero los “ejemplos clásicos” como su C ++ son casi siempre estructuras inventadas diseñadas para transformar el paradigma en cerebros de estudiantes de pregrado para que no comiencen a quejarse de cuán estúpidos son los lenguajes que se supone que deben usar .

La idea parece básicamente modelar “objetos del mundo real” por objetos en su lenguaje de progtwigción. Lo cual puede ser un buen enfoque para los problemas de progtwigción reales, pero solo tiene sentido si de hecho puede dibujar una analogía entre cómo usaría el objeto del mundo real y cómo se manejan los objetos OO dentro del progtwig.

Lo cual es simplemente ridículo para tales ejemplos de animales. En todo caso, los métodos tendrían que ser algo así como “alimentar”, “leche”, “matanza” … pero “transporte” es un nombre inapropiado, tomaría eso para mover realmente al animal, que sería más bien un método del medio ambiente en el que vive el animal, y básicamente tiene sentido solo como parte de un patrón de visitante.

describe , type y lo que llamas transport son, por otro lado, mucho más simples. Estas son básicamente constantes dependientes del tipo o simples funciones puras. Solo OO paranoia ratifica hacerlos métodos de clase.

Cualquier cosa en la línea de este material animal, donde básicamente solo hay datos , se vuelve mucho más simple si no tratas de forzarlo en algo parecido a OO, sino que simplemente te quedas con datos (útilmente tipados) en Haskell.

Entonces, como este ejemplo obviamente no nos lleva más allá, consideremos algo donde OOP tenga sentido. Widget toolkits viene a la mente. Algo como

 class Widget; class Container : public Widget { std::vector> children; public: // getters ... }; class Paned : public Container { public: Rectangle childBoundaries(int) const; }; class ReEquipable : public Container { public: void pushNewChild(std::unique_ptr&&); void popChild(int); }; class HJuxtaposition: public Paned, public ReEquipable { ... }; 

¿Por qué OO tiene sentido aquí? En primer lugar, nos permite almacenar fácilmente una colección heterogénea de widgets. Eso en realidad no es fácil de lograr en Haskell, pero antes de intentarlo, es posible que se pregunte si realmente lo necesita. Para ciertos contenedores, tal vez no sea tan deseable permitir esto, después de todo. En Haskell, el polymorphism paramétrico es muy agradable de usar. Para cualquier tipo de widget, observamos que la funcionalidad de Container reduce a una simple lista. Entonces, ¿por qué no simplemente usar una lista, donde sea que requiera un Container ?

Por supuesto, en este ejemplo, probablemente encontrará que necesita contenedores heterogéneos; la forma más directa de obtenerlos es {-# LANGUAGE ExistentialQuantification #-} :

 data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w } 

En este caso, Widget sería una clase de tipo (podría ser una traducción bastante literal del Widget clase abstracta). En Haskell esto es más bien un último recurso que hacer, pero podría estar aquí mismo.

Paned es más una interfaz. Podríamos usar otra clase de tipos aquí, básicamente transcribiendo el C ++ uno:

 class Paned c where childBoundaries :: c -> Int -> Maybe Rectangle 

ReEquipable es más difícil, porque sus métodos realmente mutan el contenedor. Eso es obviamente problemático en Haskell. Pero, de nuevo, puede que no sea necesario: si ha sustituido la clase Container por listas simples, es posible que pueda hacer las actualizaciones como actualizaciones totalmente funcionales.

Sin embargo, probablemente esto sería demasiado ineficiente para la tarea que nos ocupa. La discusión completa de las maneras de hacer las actualizaciones mutables de manera eficiente sería demasiado para el scope de esta respuesta, pero existen tales maneras, por ejemplo, el uso de lenses .

Resumen

OO no se traduce demasiado bien a Haskell. No hay un isomorfismo genérico simple, solo múltiples aproximaciones entre las cuales elegir requiere experiencia. Con la mayor frecuencia posible, evite abordar el problema desde un ángulo OO y piense en datos, funciones y capas de mónada. Resulta que esto te lleva muy lejos en Haskell. Solo en algunas aplicaciones, OO es tan natural que vale la pena presionarlo en el idioma.


Disculpa, este tema siempre me lleva al modo de rant de opinión fuerte …

Esta paranoia está motivada en parte por los problemas de mutabilidad, que no surgen en Haskell.

En Haskell no hay un buen método para hacer “árboles” de herencia. En cambio, solemos hacer algo como

 data Animal = Animal ... data Mammal = Mammal Animal ... data Bat = Bat Mammal ... 

Entonces, recaptamos información común. Lo cual no es poco común en OOP, “favorecer la composición sobre la herencia”. A continuación, creamos estas interfaces, denominadas clases de tipo

 class Named a where name :: a -> String 

Luego, Bat instancias de Animal , Mammal y Bat de Named sin embargo, eso tenía sentido para cada uno de ellos.

A partir de ese momento, simplemente escribiremos funciones para la combinación apropiada de clases de tipo, realmente no nos importa que Bat tenga un Animal enterrado en su interior con un nombre. Nosotros solo decimos

 prettyPrint :: Named a => a -> String prettyPrint a = "I love " ++ name a ++ "!" 

y deje que las clases de tipos subyacentes se preocupen por averiguar cómo manejar los datos específicos. Esto nos permite escribir código más seguro de muchas maneras, por ejemplo

 foo :: Top -> Top bar :: Topped a => a -> a 

Con foo , no tenemos idea de qué subtipo de Top se está devolviendo, tenemos que hacer un feo casting basado en el tiempo de ejecución para resolverlo. Con la bar , garantizamos estáticamente que nos apegamos a nuestra interfaz, pero que la implementación subyacente es consistente en toda la función. Esto hace que sea mucho más fácil componer de forma segura funciones que funcionan en diferentes interfaces para el mismo tipo.

TLDR; En Haskell, comstackmos datos de tratamiento de forma más compositiva, y luego dependemos de polymorphism paramétrico restringido para garantizar la abstracción segura a través de tipos concretos sin sacrificar la información de tipo.

Hay muchas formas de implementar esto con éxito en Haskell, pero pocas se “sentirán” como Java. Aquí hay un ejemplo: modelaremos cada tipo de forma independiente pero proporcionaremos operaciones de “lanzamiento” que nos permitirán tratar subtipos de Animal como Animal

 data Animal = Animal String String String data Flying = Flying String String data Mammal = Mammal String String castMA :: Mammal -> Animal castMA (Mammal transport description) = Animal transport "Mammal" description castFA :: Flying -> Animal castFA (Flying type description) = Animal "Flying" type description 

Entonces, obviamente, puedes hacer una lista de Animal s sin problemas. A veces las personas les gusta implementar esto a través de ExistentialTypes y typeclasses

 class IsAnimal a where transport :: a -> String type :: a -> String description :: a -> String instance IsAnimal Animal where transport (Animal tr _ _) = tr type (Animal _ t _) = t description (Animal _ _ d) = d instance IsAnimal Flying where ... instance IsAnimal Mammal where ... data AnyAnimal = forall t. IsAnimal t => AnyAnimal t 

lo que nos permite inyectar Flying and Mammal directamente en una lista

 animals :: [AnyAnimal] animals = [AnyAnimal flyingType, AnyAnimal mammalType] 

pero esto en realidad no es mucho mejor que el ejemplo original ya que hemos descartado toda la información sobre cada elemento en la lista, excepto que tiene una instancia IsAnimal , que, mirando cuidadosamente, es completamente equivalente a decir que es solo un Animal .

 projectAnimal :: IsAnimal a => a -> Animal projectAnimal a = Animal (transport a) (type a) (description a) 

Así que bien podríamos haber ido con la primera solución.

Muchas otras respuestas ya insinúan cómo las clases de tipo pueden ser interesantes para usted. Sin embargo, quiero señalar que, en mi experiencia, muchas veces cuando piensas que una clase de letra es la solución a un problema, en realidad no lo es. Creo que esto es especialmente cierto para las personas con antecedentes de OOP.

De hecho, hay un artículo de blog muy popular sobre esto, Haskell Antipattern: Existential Typeclass , ¡puede que lo disfrutes!

Un enfoque más simple para su problema podría ser modelar la interfaz como un tipo de datos algebraicos simples, por ej.

 data Animal = Animal { animalTransport :: String, animalType :: String } 

Tal que tu bat convierte en un valor simple:

 flyingTransport :: String flyingTransport = "Flying" mammalType :: String mammalType = "Mammal" bat :: Animal bat = Animal flyingTransport mammalType 

Con esto a mano, puede definir un progtwig que describa cualquier animal, al igual que lo hace su progtwig:

 describe :: Animal -> String describe a = "I am a " ++ animalTransport a ++ " " ++ animalType a main :: IO () main = putStrLn (describe bat) 

Esto hace que sea fácil tener una lista de valores de Animal y, por ejemplo, imprimir la descripción de cada animal.