2010-06-26 13 views
16

Soy un desarrollador de C#. Viniendo del lado OO del mundo, comienzo pensando en términos de interfaces, clases y jerarquías de tipos. Debido a la falta de OO en Haskell, a veces me encuentro atascado y no puedo pensar en una forma de modelar ciertos problemas con Haskell.¿Cómo moderar jerarquías de clase en Haskell?

Cómo modelar, en Haskell, las situaciones de la vida real que las jerarquías de clase, como el que se muestra aquí: http://www.braindelay.com/danielbray/endangered-object-oriented-programming/isHierarchy-4.gif

+1

Depende en gran medida de las operaciones que desee realizar en ellas. ¿Puedes describir cómo te gustaría usar objetos de esas clases en tu programa? – sepp2k

+0

Escogí un ejemplo al azar. Siéntase libre de asumir cualquier operación. Solo quiero tener una idea general sobre cómo se manejan esas situaciones en Haskell. – Kibarim

Respuesta

13

Supongamos las siguientes operaciones: Los seres humanos pueden hablar, los perros pueden ladrar, y todos los miembros de una especie puede aparearse con miembros de la misma especie si tienen sexo opuesto. Yo definiría esta en Haskell como esto:

data Gender = Male | Female deriving Eq 

class Species s where 
    gender :: s -> Gender 

-- Returns true if s1 and s2 can conceive offspring 
matable :: Species a => a -> a -> Bool 
matable s1 s2 = gender s1 /= gender s2 

data Human = Man | Woman 
data Canine = Dog | Bitch 

instance Species Human where 
    gender Man = Male 
    gender Woman = Female 

instance Species Canine where 
    gender Dog = Male 
    gender Bitch = Female 

bark Dog = "woof" 
bark Bitch = "wow" 

speak Man s = "The man says " ++ s 
speak Woman s = "The woman says " ++ s 

Ahora la operación matable tiene tipo Species s => s -> s -> Bool, bark tiene tipo Canine -> String y speak tiene por tipo Human -> String -> String.

No sé si esto ayuda, pero dada la naturaleza bastante abstracta de la pregunta, es lo mejor que se me ocurre.

Editar: En respuesta al comentario de Daniel:

Una jerarquía simple para las colecciones podría tener este aspecto (haciendo caso omiso de las clases ya existentes como plegable y Functor):

class Foldable f where 
    fold :: (a -> b -> a) -> a -> f b -> a 

class Foldable m => Collection m where 
    cmap :: (a -> b) -> m a -> m b 
    cfilter :: (a -> Bool) -> m a -> m a 

class Indexable i where 
    atIndex :: i a -> Int -> a 

instance Foldable [] where 
    fold = foldl 

instance Collection [] where 
    cmap = map 
    cfilter = filter 

instance Indexable [] where 
    atIndex = (!!) 

sumOfEvenElements :: (Integral a, Collection c) => c a -> a 
sumOfEvenElements c = fold (+) 0 (cfilter even c) 

Ahora sumOfEvenElements toma cualquier tipo de colección de integrales y devuelve la suma de todos los elementos pares de esa colección.

+0

¿Qué ocurre si la jerarquía de clases tiene más de un nivel de profundidad? Los ADT no se aplicarán en tal caso, ¿verdad? – Kibarim

+0

Creo que la pregunta es más sobre cómo se vería una jerarquía de clases usando herencia en Haskell. Por ejemplo, ¿cómo se vería Iterable <- Collection <- List <- ArrayList? –

+0

Sí, eso es lo que quiero decir. Gracias Daniel por aclarar mi pregunta. – Kibarim

6

En lugar de clases y objetos, Haskell usa tipos de datos abstractos. Estos son realmente dos puntos de vista compatibles sobre el problema de organizar formas de construyendo y observando información. La mejor ayuda que conozco sobre este tema es el ensayo de William Cook Object-Oriented Programming Versus Abstract Data Types. Él tiene algunas explicaciones muy claras en el sentido de que

  • En un sistema basado en la clase, el código está organizado en torno a diferentes maneras de construir abstracciones. En general, a cada forma diferente de construcción de una abstracción se le asigna su propia clase. Los métodos solo saben cómo observar las propiedades de esa construcción.

  • En un sistema basado en ADT (como Haskell), el código se organiza en torno a diferentes formas de observando abstracciones. En general, a cada forma diferente de observar una abstracción se le asigna su propia función. La función conoce todos las formas en que se podría construir la abstracción, y sabe cómo observar una sola propiedad, pero de cualquier construcción.

El documento de Cook le mostrará un diseño de matriz agradable de abstracciones y le enseñará cómo organizar cualquier clase como ADY o viceversa.

jerarquías de clase implican un elemento más: la reutilización de implementaciones a través de la herencia.En Haskell, dicha reutilización se logra a través de funciones de primera clase: una función en una abstracción Primate es un valor y una implementación de la abstracción Human puede reutilizar cualquier función de la abstracción Primate, puede envolverlos para modificar sus resultados, etc. .

No hay un ajuste exacto entre el diseño con jerarquías de clase y el diseño con tipos de datos abstractos. Si tratas de transcribir de uno a otro, terminarás con algo incómodo y no idiomático — como un programa FORTRAN escrito en Java. Pero si comprende los principios de las jerarquías de clase y los principios de los tipos de datos abstractos, puede tomar una solución a un problema en un estilo y crear una solución razonablemente idiomática para el mismo problema en el otro estilo. Toma práctica.


Adición: También es posible utilizar el sistema de clase de tipos de Haskell para tratar de emular a las jerarquías de clase, pero eso es una harina de otro costal. Las clases de tipo son lo suficientemente similares a las clases ordinarias que funcionan una serie de ejemplos estándar, pero son lo suficientemente diferentes como para que también pueda haber algunas sorpresas e inadaptados muy grandes. Si bien las clases de tipo son una herramienta invaluable para un programador de Haskell, recomendaría que cualquiera que esté aprendiendo Haskell aprenda a diseñar programas utilizando tipos de datos abstractos.

+0

No entiendo cómo se pueden usar las funciones de primera clase como sustituto de la herencia. ¿Puedes dar un ejemplo? – Kibarim

31

Primero que nada: el diseño OO estándar no va a funcionar bien en Haskell. Puedes luchar contra el lenguaje e intentar hacer algo similar, pero será un ejercicio de frustración. El paso uno es . Busque soluciones de estilo Haskell para su problema en lugar de buscar la forma de escribir una solución estilo OOP en Haskell.

¡Pero eso es más fácil decirlo que hacerlo! ¿Dónde empezar?

Desmontamos los detalles arenosos de lo que OOP hace por nosotros y pensemos cómo se verán en Haskell.

  • Objetos: En términos generales, un objeto es la combinación de algunos datos con métodos que operan sobre esos datos. En Haskell, los datos se estructuran normalmente usando tipos de datos algebraicos; los métodos se pueden considerar como funciones que toman los datos del objeto como un argumento inicial implícito.
  • Encapsulación: Sin embargo, la capacidad de inspeccionar los datos de un objeto generalmente se limita a sus propios métodos. En Haskell, hay varias maneras de ocultar un fragmento de datos, dos ejemplos son:
    • definir el tipo de datos en un módulo separado que no exporta constructores del tipo. Solo las funciones en ese módulo pueden inspeccionar o crear valores de ese tipo. Esto es algo comparable a los miembros protected o internal.
    • Utilice la aplicación parcial. Considere la función map con sus argumentos invertidos. Si lo aplica a una lista de Int s, obtendrá una función del tipo (Int -> b) -> [b]. La lista que le diste todavía está "allí", en cierto sentido, pero nada más puede usarla excepto a través de la función. Esto es comparable a los miembros private, y la función original que se está aplicando parcialmente es comparable a un constructor de estilo OOP.
  • "Ad-hoc" polimorfismo: A menudo, en la programación OO sólo nos importa que algo implementa un método; cuando lo llamamos, el método específico llamado se determina en función del tipo real. Haskell proporciona clases de tipo para la sobrecarga de funciones en tiempo de compilación, que son en muchos sentidos más más flexibles que las que se encuentran en los lenguajes de OOP.
  • Reutilización de código: Honestamente, mi opinión es que la reutilización de código a través de la herencia fue y es un error. Los mix-ins que se encuentran en algo como Ruby me parecen una mejor solución de OO. En cualquier caso, en cualquier lenguaje funcional, el enfoque estándar es factorizar comportamientos comunes utilizando funciones de orden superior, luego especializar la forma de propósito general. Un ejemplo clásico aquí son las funciones fold, que generalizan casi todos los bucles iterativos, las transformaciones de lista y las funciones lineales recursivas.
  • Interfaces: Dependiendo de cómo se está utilizando una interfaz, hay diferentes opciones:
    • Para desacoplar aplicación: funciones polimórficas con las limitaciones de clase de tipos son lo que queremos aquí. Por ejemplo, la función sort tiene el tipo (Ord a) => [a] -> [a]; está completamente desacoplado de los detalles del tipo que le da, además de que debe ser una lista de algún tipo que implemente Ord.
    • Trabajar con varios tipos con una interfaz compartida: Para ello necesita ya sea una extensión del lenguaje de tipos existenciales, o que sea sencillo, utiliza alguna variación en la aplicación parcial que el anterior - en lugar de los valores y funciones que puede aplicar a ellos, aplicar las funciones antes de tiempo y trabajar con los resultados.
  • Subtificación, también denominado el "es-un" relación: Aquí es donde usted está en su mayoría fuera de suerte. Pero, hablando por experiencia, haber sido un desarrollador profesional de C# durante años, los casos en los que realmente necesita subtipos no son terriblemente comunes. En su lugar, piense en lo anterior y qué comportamiento está tratando de capturar con la relación de subtipado.

También puede encontrar this blog post útil; proporciona un resumen rápido de lo que utilizaría en Haskell para resolver los mismos problemas que algunos patrones de diseño estándar a menudo se utilizan en OOP.

Como apéndice final, como programador de C#, puede resultarle interesante investigar las conexiones entre él y Haskell. Muchas personas responsables de C# también son programadores de Haskell, y algunas adiciones recientes a C# fueron muy influenciadas por Haskell. Lo más notable es probablemente la estructura monádica subyacente a LINQ, con IEnumerable siendo esencialmente la lista de mónadas.

+0

Guau, ahora que es un análisis en profundidad. Tenemos la misma opinión sobre la herencia, la combinación de la interfaz y el comportamiento es una opción terrible ... –

+0

¡Excelente respuesta! Creo que las clases de tipos son * paramétricas * polimorfismo, por cierto. –

+0

@Jonathan Sterling: En realidad, no: el polimorfismo paramétrico significa abstraer sobre tipos opacos sin ningún conocimiento de lo que son; la garantía correspondiente es que la función se comportará de manera idéntica para todos los parámetros de tipo. Esto es solo variables de tipo regulares en Haskell, o "genéricos" en varios idiomas. Polimorfismo Ad-hoc significa elegir diferentes comportamientos para diferentes tipos, que utiliza clases de tipos en Haskell o funciones sobrecargadas en varios otros idiomas. –

2

Haskell es mi idioma favorito, es un lenguaje funcional puro. No tiene efectos secundarios, no hay asignación. Si encuentra difícil la transición a este idioma, tal vez F # es un mejor lugar para comenzar con la programación funcional. F # no es puro.

Los objetos encapsulan estados, hay una manera de lograr esto en Haskell, pero este es uno de los problemas que lleva más tiempo aprender porque debe aprender algunos conceptos de teoría de categorías para comprender profundamente las mónadas.Hay azúcar sintáctico que te permite ver las mónadas como una tarea no destructiva, pero en mi opinión es mejor dedicar más tiempo a entender la base de la teoría de categorías (la noción de categoría) para obtener una mejor comprensión.

Antes de intentar programar en estilo OO en Haskell, debería preguntarse si realmente usa el estilo orientado a objetos en C#, muchos programadores usan lenguajes OO, pero sus programas están escritos en el estilo estructurado.

La declaración de datos le permite definir estructuras de datos que combinan productos (equivalentes a estructura en lenguaje C) y uniones (equivalentes a unión en C), la parte derivada de la declaración permite heredar métodos predeterminados.

Un tipo de datos (estructura de datos) pertenece a una clase si tiene una implementación del conjunto de métodos en la clase. Por ejemplo, si puede definir un método show :: a -> String para su tipo de datos, entonces pertenece a la clase Show, puede definir su tipo de datos como una instancia de la clase Show.

Esto es diferente del uso de la clase en algunos lenguajes OO donde se usa como una forma de definir estructuras + métodos.

Un tipo de datos es abstracto si es independiente de su implementación. Puede crear, modificar y destruir el objeto mediante una interfaz abstracta, no necesita saber cómo se implementa.

La abstracción es compatible con Haskell, es muy fácil de declarar. Por ejemplo el código desde el sitio Haskell:

data Tree a = Nil 
      | Node { left :: Tree a, 
        value :: a, 
        right :: Tree a } 

declara los selectores izquierda, el valor, a la derecha. los constructores pueden definirse de la siguiente manera si quiere añadirlos a la lista de exportación en la declaración de módulo:

node = Node 
nil = Nil 

módulos están construidos de una manera similar a la del Modula. Aquí hay otro ejemplo del mismo sitio:

module Stack (Stack, empty, isEmpty, push, top, pop) where 

empty :: Stack a 
isEmpty :: Stack a -> Bool 
push :: a -> Stack a -> Stack a 
top :: Stack a -> a 
pop :: Stack a -> (a,Stack a) 

newtype Stack a = StackImpl [a] -- opaque! 
empty = StackImpl [] 
isEmpty (StackImpl s) = null s 
push x (StackImpl s) = StackImpl (x:s) 
top (StackImpl s) = head s 
pop (StackImpl (s:ss)) = (s,StackImpl ss) 

¡Hay más para decir sobre este tema, espero que este comentario ayude!

Cuestiones relacionadas