2010-05-11 16 views
17

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.Manejo incremental Modelado de datos Cambios en la programación funcional

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 puedo minimizar este problema?

Esto parece ser un problema reconocido como Xavier Leroy states nicely on page 24 of "Objects and Classes vs. Modules" - Para resumir para aquellos que no tienen un visor PostScript que básicamente dice idiomas FP son mejores que los lenguajes de programación orientada a objetos para añadir un nuevo comportamiento por encima de objetos de datos, pero están mejor lenguajes OOP para agregar nuevos objetos/propiedades de datos.

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

He leído Phillip Wadler's recommendation of using Monads para ayudar con este problema de modularidad, pero no estoy seguro de entender cómo hacerlo.

+0

Algunas discusiones sobre el problema de la expresión aquí: http://lambda-the-ultimate.org/node/1641 parece ayudar, pero todavía no hay respuesta. –

+0

Quizás una solución sería mejores herramientas de refactorización para lenguajes FP. Al menos para Haskell y ML tengo el compilador –

Respuesta

22

Como Darius Bacon observado, este es esencialmente el problema de 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 algebraica:

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 sencillas, que sólo contienen valores literales y adición. Con las funciones que tenemos aquí, podemos tomar una expresión y evaluarla, o mostrarla como 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 tan 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 es más interesante que agregar funciones que las usan. 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 up-end todo e invertimos 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 valores de expresión con uno que represente acciones en la expresión. En lugar de elegir un constructor que 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 crear 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" utilizando las funciones regulares, devolver un registro de las 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)) 

se puede añadir 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 - que se olvidó de añadir una acción actShow anterior, vamos a añadir que, en, sólo tendremos que ... errh, también.

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 las está extendiendo.

Como una nota interesante, tenga en cuenta que en el estilo de "acciones", los valores reales en la expresión están completamente ocultos --el campo actEval sólo promete darnos algo del tipo correcto, la forma en que se ofrece es su propio negocio Gracias a la evaluación perezosa, el contenido del campo puede ser incluso un cálculo elaborado, realizado solo a pedido. Un valor Actions a es completamente opaco a 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 ello, pero no puedo recordar ...

+0

Parece mucho más obvio ahora ... en FP si tienes un problema, simplemente usa más funciones como direccionamiento indirecto :) El problema que creo es que uso lenguajes OOP tanto para mi trabajo diario que me olvido de cómo pensar "funcional".Muchas gracias por la excelente respuesta. –

+2

@Adam Gent: La divertida ironía de todo esto es que, con respecto a un "objeto" como un conjunto de métodos, como con 'Actions a' en mi respuesta, la programación diaria en OOP hace * mucho * más uso extensivo de cierres y funciones de orden superior que es común en FP, particularmente lenguajes de familias ML que enfatizan la coincidencia de patrones y tipos de datos algebraicos. –

+3

@Adam Gent: También debería tener en cuenta que mi segundo ejemplo se construyó para resaltar la simetría y mostrar cómo se ve el problema típico de la terminación de POO en Haskell. En realidad, usar ese estilo en Haskell es algo doloroso, en parte debido al terrible sistema de registro de Haskell. Sin embargo, para fines prácticos, el punto general es válido: no asuma que debe reemplazar objetos con tipos de datos algebraicos, sino que considere funciones de orden superior parcialmente aplicadas que encapsulen el propósito del objeto. –

4

Este equilibrio se conoce en la literatura en lengua-teoría de la programación como el expression problem:

El objetivo es definir un tipo de datos de los casos, en los que se puede añadir nuevos casos al tipo de datos y nuevas funciones durante el tipo de datos, sin recompilar el código existente, y al mismo tiempo conservar la seguridad del tipo estático (por ejemplo, sin conversiones).

Se han presentado soluciones, pero no las he estudiado. (Mucha discusión at Lambda The Ultimate.)

+0

Sí, he visto la solución de Scala para esto. Me siento reticente de que no hice una "coincidencia de problemas" con el problema de expresión. –

+0

¡Me siento un poco engañado por no haber investigado tanto! –

6

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

En la programación funcional parece que la adición de nuevas propiedades de datos requiere una gran cantidad de cambios en el código porque de coincidencia de patrones y datos constructores (Haskell, ML).

Pero esto es en gran medida una característica, ¡y no es un error! Cuando se modifican 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.

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.

+0

Estoy de acuerdo con usted hasta cierto punto que esto es algunas veces una característica. Esto funciona muy bien para proyectos de equipos pequeños en los que no está creando un framework/api (la única molestia aquí sería la falta de herramientas de refactorización para Haskell). Sin embargo, si necesita hacer un cambio de API/Framework para agregar un campo adicional a un registro que no es necesario con tanta frecuencia e incluso tiene un valor predeterminado, no será retrocompatible con versiones anteriores. Se requerirá que todos los que usaron la API recompilen y publiquen para un cambio de datos opcional simple. –

+0

Todavía creo que obtendrás una mejor respuesta si preguntas esto en el contexto de un problema concreto. Creo que al abstraer de forma adecuada, debería ser capaz de obtener la evolución del código de bajo impacto que desee. Pero es difícil discutir tales cosas en abstracto. – zrr

3

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.

+0

Vaya, bajé por completo de forma accidental, pero de alguna manera mi voto está bloqueado. ¿Podrías editar la pregunta para poder deshacer el downvote? – thSoft

0

Si los nuevos datos no implican un nuevo comportamiento, como en una aplicación donde 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 del 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.

Déjame explicarte: si la fecha de nacimiento es algo que implica un comportamiento de aplicación diferente, p. que hacemos algo diferente si la persona es menor de edad, luego en OOP agregaríamos un campo de fecha de nacimiento a la clase de persona, y en FP agregaríamos de manera similar un campo de fecha de nacimiento a la estructura de datos de una persona.

Si no hay ningún comportamiento asociado a "fecha de nacimiento", no debe haber ningún campo denominado "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 utilizando un pequeño lenguaje de validación similar para expresar en los datos cuál debería ser el comportamiento de validación.

Cuestiones relacionadas