2010-11-06 13 views
41

¿Hay alguna ventaja en el uso de programación orientada a objetos (OOP) en un contexto de programación funcional (FP)?¿Programación orientada a objetos en un contexto de programación puramente funcional?

He estado usando F# desde hace algún tiempo, y he notado que cuanto más mis funciones son sin estado, menos necesito tenerlas como métodos de objetos. En particular, existen ventajas al confiar en la inferencia de tipo para poder utilizarlas en la mayor cantidad de situaciones posible.

Esto no excluye la necesidad de espacios de nombres de alguna forma, que es ortogonal a ser OOP. Tampoco se desaconseja el uso de estructuras de datos. De hecho, el uso real de los lenguajes de PF depende en gran medida de las estructuras de datos. Si observa la pila F # implementada en F Sharp Programming/Advanced Data Structures, verá que no está orientada a objetos.

En mi opinión, OOP está fuertemente asociado con tener métodos que actúan sobre el estado del objeto principalmente a mutar el objeto. En un contexto de FP puro que no es necesario ni deseado.

Una razón práctica puede ser poder interactuar con el código OOP, de la misma manera que F # funciona con .NET. Aparte de eso, ¿hay alguna razón? ¿Y cuál es la experiencia en el mundo Haskell, donde la programación es más pura FP?

Agradeceré cualquier referencia a documentos o ejemplos contrafactuales del mundo real sobre el tema.

Respuesta

52

La desconexión que ves no es de FP vs. OOP. Se trata principalmente de inmutabilidad y formalismos matemáticos vs. mutabilidad y enfoques informales.

Primero, dejemos de lado el problema de la mutabilidad: puede tener FP con mutabilidad y OOP con inmutabilidad bien. Aún más funcional que Haskell te permite jugar con datos mutables todo lo que quieras, solo tienes que ser explícito sobre qué es mutable y el orden en que suceden las cosas; y dejando a un lado las preocupaciones sobre la eficiencia, casi cualquier objeto mutable podría construir y devolver una nueva instancia "actualizada" en lugar de cambiar su propio estado interno.

El problema más grande aquí son los formalismos matemáticos, en particular el uso intensivo de tipos de datos algebraicos en un lenguaje poco alejado del cálculo lambda. Has etiquetado esto con Haskell y F #, pero te das cuenta de que es solo la mitad del universo de programación funcional; la familia Lisp tiene un carácter muy diferente, mucho más libre en comparación con los lenguajes de estilo ML. La mayoría de los sistemas OO de amplio uso en la actualidad son de naturaleza muy informal: existen formalismos para OO, pero no se mencionan explícitamente de la misma forma que los formalismos de FP en los lenguajes de estilo ML.

Muchos de los conflictos aparentes simplemente desaparecen si elimina la discrepancia de formalismo. ¿Desea construir un sistema OO flexible, dinámico y ad-hoc sobre un Lisp? Adelante, funcionará bien. ¿Desea agregar un sistema OO formal e inmutable a un lenguaje ML-style? No hay problema, simplemente no esperes que funcione bien con .NET o Java.


Ahora, usted puede preguntarse, ¿qué es un formalismo apropiado para programación orientada a objetos? Bueno, aquí está la línea de golpe: en muchos sentidos, ¡está más centrada en las funciones que la FP de estilo ML! Volveré a referirme al one of my favorite papers por lo que parece ser la distinción clave: los datos estructurados como los tipos de datos algebraicos en los lenguajes de estilo ML proporcionan una representación concreta de los datos y la capacidad de definir operaciones en él; los objetos proporcionan una abstracción de recuadro negro sobre el comportamiento y la capacidad de reemplazar fácilmente los componentes.

Hay una dualidad aquí que va más allá que solo FP vs. OOP: Está estrechamente relacionada con lo que algunos teóricos del lenguaje de programación llaman the Expression Problem: Con datos concretos, puede agregar fácilmente nuevas operaciones que trabajan con él, pero cambiando la estructura de los datos Es más dificil. Con los objetos, puede agregar fácilmente nuevos datos (por ejemplo, nuevas subclases) pero es difícil agregar nuevas operaciones (piense en agregar un nuevo método abstracto a una clase base con muchos descendientes).

La razón por la que digo que la OOP está más centrada en las funciones es que las mismas funciones representan una forma de abstracción conductual. De hecho, puede simular la estructura de estilo OO en algo como Haskell utilizando registros que contienen un conjunto de funciones como objetos, permitiendo que el tipo de registro sea una "interfaz" o una "clase base abstracta" y que las funciones que crean registros reemplacen constructores de clase. Entonces, en ese sentido, los lenguajes OO usan funciones de orden superior mucho, mucho más a menudo que, por ejemplo, Haskell.

Para ver un ejemplo de algo así como este tipo de diseño muy bien utilizado en Haskell, lea la fuente de the graphics-drawingcombinators package, en particular la forma en que utiliza un tipo de registro opaco que contiene funciones y combina cosas solo en términos de su comportamiento.


EDIT: Un par de cosas finales Me olvidé de mencionar anteriormente.

Si OO hace un uso extensivo de funciones de orden superior, al principio podría parecer que debería encajar de forma muy natural en un lenguaje funcional como Haskell. Lamentablemente, este no es el caso. Es cierto que los objetos como los describí (véase el documento mencionado en el enlace LtU) encajan perfectamente. de hecho, el resultado es un estilo OO más puro que la mayoría de los lenguajes OO, porque los "miembros privados" están representados por valores ocultos por el cierre utilizado para construir el "objeto" y son inaccesibles a cualquier otra cosa que no sea la instancia específica. ¡No se obtiene mucho más privado que eso!

Lo que no funciona muy bien en Haskell es subtipificación. Y, aunque creo que la herencia y la subtipificación se usan con demasiada frecuencia en los lenguajes OO, alguna forma de subtipado es bastante útil para poder combinar objetos de manera flexible. Haskell carece de una noción inherente de subtipado, y los reemplazos hechos a mano tienden a ser excesivamente torpes para trabajar con ellos.

Como un lado, la mayoría de los lenguajes OO con sistemas de tipo estático también hacen un hash completo de subtipado al ser demasiado flojos con la posibilidad de sustitución y no proporcionar soporte adecuado para la variación en las firmas de métodos. De hecho, creo que el único lenguaje completo de OO que no lo ha estropeado completamente, al menos que yo sepa, es Scala (F # parecía hacer demasiadas concesiones a .NET, aunque al menos no creo hace nuevos errores). Sin embargo, tengo una experiencia limitada con muchos de esos idiomas, así que definitivamente podría estar equivocado aquí.

En una nota específica de Haskell, sus "clases de tipos" a menudo parecen tentadores para los programadores OO, a lo que les digo: No vayan allí. Tratar de implementar OOP de esa manera solo terminará en lágrimas. Piense en clases de tipos como reemplazo de funciones/operadores sobrecargados, no OOP.

+4

+1, ¡Gran respuesta! – missingfaktor

+0

Gracias por la impresionante respuesta (y los enlaces). Probablemente tendré preguntas de seguimiento después de leer el material. –

6

Creo que hay varias formas de entender lo que significa OOP. Para mí, no se trata de encapsular estado mutable, sino más información sobre organización y estructuración de programas. Este aspecto de OOP se puede usar perfectamente bien junto con los conceptos de FP.

Creo que mezclar los dos conceptos en F # es un enfoque muy útil: puede asociar estado inmutable con operaciones que funcionen en ese estado. Obtendrás las bonitas características de la compleción de 'punto' para los identificadores, la capacidad de usar fácilmente el código F # de C#, etc., pero aún puedes hacer que tu código funcione perfectamente. Por ejemplo, puede escribir algo como:

type GameWorld(characters) = 
    let calculateSomething character = 
    // ... 
    member x.Tick() = 
    let newCharacters = characters |> Seq.map calculateSomething 
    GameWorld(newCharacters) 

En un principio, la gente no suele declaran tipos en F # - se puede empezar con sólo escribir funciones y más tarde evolucionar su código de usarlos (cuando a entender mejor el dominio y saber cuál es la mejor forma de estructurar el código). El ejemplo anterior:

  • pasa de ser puramente funcional (el estado es una lista de caracteres y no está mutado)
  • Está orientada a objetos - lo único inusual es que todos los métodos de devolución de una nueva instancia de "el mundo"
+2

No soy muy positivo al respecto, así que permítanme hacer algunos comentarios. Los programas de organización y estructuración se pueden hacer mediante espacios de nombres o módulos en F #. La ventaja de completar puntos se contrarresta mediante el uso de 'Seq.map' en lugar de' fun x -> x.map' en las tuberías. Es una cuestión de estilo si una función que devuelve una nueva instancia debe ser parte de la clase, no solo un módulo "espacio de nombres". La compatibilidad con C# /. Net es una buena razón, por supuesto. –

8

En cuanto a Haskell, las clases son menos útiles allí porque algunas características OO se logran más fácilmente de otras maneras.

La encapsulación o "ocultación de datos" se realiza frecuentemente a través de cierres de funciones o tipos existenciales, en lugar de miembros privados. Por ejemplo, aquí hay un tipo de datos de generador de números aleatorios con estado encapsulado. El RNG contiene un método para generar valores y un valor de inicialización. Como el tipo 'semilla' está encapsulado, lo único que puede hacer con él es pasárselo al método.

data RNG a where RNG :: (seed -> (a, seed)) -> seed -> RNG a

despacho método dinámico en el contexto de polimorfismo paramétrico o "programación genérica" ​​es proporcionado por clases de tipo (que no son clases OO). Una clase de tipo es como la tabla de métodos virtuales de una clase OO. Sin embargo, no hay datos escondidos. Las clases de tipo no "pertenecen" a un tipo de datos como lo hacen los métodos de clase.

data Coordinate = C Int Int 

instance Eq Coordinate where C a b == C d e = a == b && d == e 

despacho método dinámico en el contexto de polimorfismo de subtipos o "subclases" es casi una traducción del patrón de clase en Haskell usando registros y funciones.

 
-- An "abstract base class" with two "virtual methods" 
data Object = 
    Object 
    { draw :: Image -> IO() 
    , translate :: Coord -> Object 
    } 

-- A "subclass constructor" 
circle center radius = Object draw_circle translate_circle 
    where 
    -- the "subclass methods" 
    translate_circle center radius offset = circle (center + offset) radius 
    draw_circle center radius image = ... 
+0

No conozco Haskell, ¿puede explicar más la diferencia entre el envío de métodos dinámicos y el OOP de clase estándar? –

+0

Por "envío dinámico de métodos" me refiero a una estrategia para seleccionar el método correcto para llamar en tiempo de ejecución. De las dos estrategias que se muestran aquí, la estrategia de subtipificación es como la forma en que los métodos virtuales funcionan bajo el capó en OOP (con tablas de métodos virtuales). La estrategia del polimorfismo paramétrico no está orientada a objetos, y se usa con mayor frecuencia en Haskell. – Heatsink

+3

Pequeña objeción a la terminología: "polimorfismo paramétrico" significa cuantificación universal sobre tipos opacos, lo que los lenguajes OO a menudo llaman genéricos, y es lo que Haskell tiene de forma predeterminada. La sobrecarga de tipo, ya sea de tiempo de compilación o de tiempo de ejecución, se denomina "polimorfismo ad-hoc" y es para lo que son las clases de tipos de Haskell. –

Cuestiones relacionadas