ventajas programacion orientada objetos libros funcional ejemplos desventajas haskell functional-programming oop data-modeling ocaml

haskell - programacion - Manejo incremental de modelado de datos Cambios en la programación funcional



programacion funcional ventajas y desventajas (5)

Como señaló Darius Bacon , este es esencialmente el problema de la expresión, un problema de larga data sin una solución universalmente aceptada. Sin embargo, la falta de un enfoque del mejor de los dos mundos no nos impide, a veces, querer ir de un lado a otro. Ahora, solicitó un "patrón de diseño para lenguajes funcionales" , así que echemos un vistazo. El ejemplo que sigue está escrito en Haskell, pero no necesariamente es idiomático para Haskell (o cualquier otro idioma).

Primero, una revisión rápida del "problema de expresión". Considere el siguiente tipo de datos algebraicos:

data Expr a = Lit a | Sum (Expr a) (Expr a) exprEval (Lit x) = x exprEval (Sum x y) = exprEval x + exprEval y exprShow (Lit x) = show x exprShow (Sum x y) = unwords ["(", exprShow x, " + ", exprShow y, ")"]

Esto representa expresiones matemáticas simples, que contienen solo valores literales y suma. Con las funciones que tenemos aquí, podemos tomar una expresión y evaluarla, o mostrarla como una String . Ahora, digamos que queremos agregar una nueva función, por ejemplo, mapear una función sobre todos los valores literales:

exprMap f (Lit x) = Lit (f x) exprMap f (Sum x y) = Sum (exprMap f x) (exprMap f y)

¡Fácil! ¡Podemos seguir escribiendo funciones todo el día sin romper a sudar! ¡Los tipos de datos algebraicos son increíbles!

De hecho, son geniales, queremos que nuestro tipo de expresión sea más, errh, expresivo. Vamos a extenderlo para apoyar la multiplicación, vamos a ... uhh ... oh querido, eso va a ser incómodo, ¿no? Tenemos que modificar cada función que acabamos de escribir. ¡Desesperación!

De hecho, tal vez la extensión de las expresiones sea más interesante que agregar funciones que las usen. Entonces, digamos que estamos dispuestos a hacer la compensación en la otra dirección. ¿Cómo podríamos hacer eso?

Bueno, no tiene sentido hacer las cosas a medio camino. Vamos a poner fin a todo e invertir todo el programa. Qué significa eso? Bueno, esto es programación funcional, ¿y qué es más funcional que las funciones de orden superior? Lo que haremos es reemplazar el tipo de datos que representa los valores de expresión por uno que represente acciones en la expresión . En lugar de elegir un constructor, necesitaremos un registro de todas las acciones posibles, algo como esto:

data Actions a = Actions { actEval :: a, actMap :: (a -> a) -> Actions a }

Entonces, ¿cómo creamos una expresión sin un tipo de datos? Bueno, nuestras funciones ahora son datos, así que supongo que nuestros datos deben ser funciones. Haremos "constructores" usando funciones regulares, devolviendo un registro de acciones:

mkLit x = Actions x (/f -> mkLit (f x)) mkSum x y = Actions (actEval x + actEval y) (/f -> mkSum (actMap x f) (actMap y f))

¿Podemos agregar la multiplicación más fácilmente ahora? Claro que sí!

mkProd x y = Actions (actEval x * actEval y) (/f -> mkProd (actMap x f) (actMap y f))

Oh, pero espera, nos olvidamos de agregar una acción actShow antes, vamos a agregar eso, vamos a ... errh, bueno.

En cualquier caso, ¿qué aspecto tiene utilizar los dos estilos diferentes?

expr1plus1 = Sum (Lit 1) (Lit 1) action1plus1 = mkSum (mkLit 1) (mkLit 1) action1times1 = mkProd (mkLit 1) (mkLit 1)

Más o menos lo mismo, cuando no los estás extendiendo.

Como nota al margen interesante, considere que en el estilo de "acciones", los valores reales en la expresión están completamente ocultos actEval campo actEval solo promete darnos algo del tipo correcto, cómo lo proporciona es asunto suyo. Gracias a la evaluación perezosa, el contenido del campo puede ser incluso un cálculo elaborado, realizado solo a pedido. Un valor de Actions a es completamente opaco para la inspección externa, presentando solo las acciones definidas al mundo exterior.

Este estilo de programación: reemplaza datos simples con un conjunto de "acciones" mientras oculta los detalles reales de implementación en una caja negra, usando funciones tipo constructor para construir nuevos bits de datos, pudiendo intercambiar "valores" muy diferentes con el mismo conjunto de "acciones", etc. - es interesante. Probablemente haya un nombre para eso, pero no puedo recordar ...

La mayoría de los problemas que tengo que resolver en mi trabajo como desarrollador tienen que ver con el modelado de datos. Por ejemplo, en un mundo de aplicaciones web de OOP, a menudo tengo que cambiar las propiedades de los datos que están en un objeto para cumplir con los nuevos requisitos.

Si tengo suerte, ni siquiera necesito agregar programáticamente un nuevo código de "comportamiento" (funciones, métodos). En cambio, puedo declarar la validación de agregar e incluso las opciones de IU anotando la propiedad (Java).

En la Programación Funcional, parece que agregar nuevas propiedades de datos requiere muchos cambios de código debido a la coincidencia de patrones y los constructores de datos (Haskell, ML).

¿Cómo minimizo este problema?

Esto parece ser un problema reconocido, como bien dice Xavier Leroy en la página 24 de "Objetos y clases frente a módulos" : para resumir para aquellos que no tienen un visor PostScript, básicamente dice que los lenguajes FP son mejores que los lenguajes OOP para agregar nuevos comportamiento sobre objetos de datos, pero los lenguajes OOP son mejores para agregar nuevos objetos / propiedades de datos.

¿Hay algún patrón de diseño utilizado en los lenguajes de FP para ayudar a mitigar este problema?

He leído la recomendación de Phillip Wadler de utilizar las Mónadas para ayudar con este problema de modularidad, pero no estoy seguro de entender cómo hacerlo.


En Haskell, al menos, haría un tipo de datos abstracto. Eso es crear un tipo que no exporta constructores. Los usuarios del tipo pierden la capacidad de coincidencia de patrones en el tipo y debe proporcionar funciones para trabajar con el tipo. A cambio, obtiene un tipo que es más fácil de modificar sin cambiar el código escrito por los usuarios del tipo.


Esta compensación es conocida en la literatura de la teoría del lenguaje de programación como el problema de expresión :

El objetivo es definir un tipo de datos por casos, donde uno puede agregar nuevos casos al tipo de datos y nuevas funciones sobre el tipo de datos, sin recompilar el código existente, y al mismo tiempo mantener la seguridad del tipo estático (por ejemplo, sin conversiones).

Se han presentado soluciones, pero no las he estudiado. (Mucho debate en Lambda The Ultimate .)


He escuchado esta queja más de unas pocas veces, y siempre me confunde. El interlocutor escribió:

En la Programación Funcional, parece que agregar nuevas propiedades de datos requiere muchos cambios de código debido a la coincidencia de patrones y los constructores de datos (Haskell, ML).

Pero esto es, en general, una característica, ¡y no un error! Cuando se cambian las posibilidades en una variante, por ejemplo, el código que accede a esa variante a través de la coincidencia de patrones se ve obligado a considerar el hecho de que han surgido nuevas posibilidades. Esto es útil, porque de hecho necesita considerar si ese código necesita cambiar para reaccionar a los cambios semánticos en los tipos que manipula.

Yo discutiría con la afirmación de que se requieren "muchos cambios de código". Con un código bien escrito, el sistema de tipos generalmente hace un trabajo impresionantemente bueno para resaltar el código que debe considerarse, y no mucho más.

Quizás el problema aquí es que es difícil responder la pregunta sin un ejemplo más concreto. Considere proporcionar un código en Haskell o ML que no esté seguro de cómo evolucionar limpiamente. Me imagino que obtendrás respuestas más precisas y útiles de esa manera.


Si los nuevos datos no implican un nuevo comportamiento, como en una aplicación en la que se nos pide agregar un campo "fecha de nacimiento" a un recurso "persona" y luego todo lo que tenemos que hacer es agregarlo a una lista de campos que son parte de el recurso persona, entonces es fácil de resolver tanto en el mundo funcional como en el OOP. Simplemente no trates la "fecha de nacimiento" como parte de tu código; es solo parte de tus datos.

Permítanme explicar: si la fecha de nacimiento es algo que implica un comportamiento de aplicación diferente, por ejemplo, si hacemos algo diferente si la persona es menor de edad, entonces en OOP agregaríamos un campo de fecha de nacimiento a la clase de persona, y en FP agregaríamos campo de fecha de nacimiento a una estructura de datos de persona.

Si no hay un comportamiento adjunto a "fecha de nacimiento", entonces no debe haber ningún campo llamado "fecha de nacimiento" en el código. Una estructura de datos como un diccionario (un mapa) contendría los diversos campos. Agregar uno nuevo no requeriría cambios de programa, sin importar si es OOP o FP. Las validaciones se agregarían de manera similar, adjuntando una expresión regular de validación o usando un pequeño lenguaje de validación similar para expresar en los datos cuál debería ser el comportamiento de validación.