Vengo de un entorno orientado a objetos donde aprendí que las clases son o al menos se pueden usar para hacer una capa de abstracción que permite un reciclaje fácil del código que luego se puede usar para hacer objetos o en herencia.
Por ejemplo, puedo tener una clase de animales y luego heredar gatos y perros, y todos heredan muchos de los mismos rasgos, y de esas subclases puedo hacer objetos que puedan especificar una raza de animal o incluso el nombre de eso.
O puedo usar clases para especificar varias instancias del mismo código que maneja o contiene cosas ligeramente diferentes; como nodos en un árbol de búsqueda o múltiples conexiones de bases de datos diferentes y qué no.
Recientemente me mudé a la programación funcional, así que comencé a preguntarme:
¿cómo manejan los lenguajes puramente funcionales cosas así? Es decir, lenguajes sin ningún concepto de clases y objetos.
fuente
Respuestas:
Muchos lenguajes funcionales tienen un sistema de módulos. (Por cierto, muchos lenguajes orientados a objetos también lo hacen). Pero incluso en ausencia de uno, puede usar funciones como módulos.
JavaScript es un buen ejemplo. En JavaScript, las funciones se utilizan tanto para implementar módulos como para la encapsulación orientada a objetos. En Scheme, que fue la principal inspiración para JavaScript, solo hay funciones. Las funciones se utilizan para implementar casi todo: objetos, módulos (llamados unidades en Racket), incluso estructuras de datos.
OTOH, Haskell y la familia ML tienen un sistema de módulos explícito.
La orientación a objetos se trata de la abstracción de datos. Eso es. La modularidad, la herencia, el polimorfismo, incluso el estado mutable son preocupaciones ortogonales.
fuente
module
. Creo que es lamentable que Racket tenga un concepto llamadomodule
que no es un módulo, y un concepto que es un módulo pero no llamadomodule
. De todos modos, escribió: "las unidades están a medio camino entre los espacios de nombres y las interfaces OO". Bueno, ¿no es ese el tipo de definición de qué es un módulo?map
y una unidad dependiente de un enlacemap
son diferentes en el sentido de que el módulo debe referirse a algúnmap
enlace específico , como el deracket/base
, mientras que diferentes usuarios de la unidad pueden dar diferentes definiciones demap
la unidad.Parece que estás haciendo dos preguntas: "¿Cómo puedes lograr la modularidad en lenguajes funcionales?" que se ha tratado en otras respuestas y "¿cómo puedes crear abstracciones en lenguajes funcionales?" Lo cual responderé.
En los idiomas OO, tiendes a concentrarte en el sustantivo, "un animal", "el servidor de correo", "su tenedor de jardín", etc. Los idiomas funcionales, por el contrario, enfatizan el verbo, "caminar", "buscar correo" , "pinchar", etc.
No es sorprendente, entonces, que las abstracciones en lenguajes funcionales tiendan más a verbos u operaciones que a cosas. Un ejemplo que siempre busco cuando intento explicar esto es el análisis. En lenguajes funcionales, una buena forma de escribir analizadores es especificando una gramática y luego interpretándola. El intérprete crea una abstracción sobre el proceso de análisis.
Otro ejemplo concreto de esto es un proyecto en el que estaba trabajando hace poco. Estaba escribiendo una base de datos en Haskell. Tenía un 'lenguaje incrustado' para especificar operaciones en el nivel más bajo; por ejemplo, me permitió escribir y leer cosas desde el medio de almacenamiento. Tenía otro 'lenguaje incrustado' separado para especificar operaciones al más alto nivel. Luego tuve, lo que es esencialmente un intérprete, para convertir operaciones del nivel superior al nivel inferior.
Esta es una forma notablemente general de abstracción, pero no es la única disponible en lenguajes funcionales.
fuente
Aunque la "programación funcional" no transmite implicaciones de largo alcance para los problemas de modularidad, los lenguajes particulares abordan la programación en general de diferentes maneras. La reutilización y la abstracción del código interactúan porque cuanto menos expongas, más difícil será reutilizar el código. Dejando a un lado la abstracción, abordaré dos cuestiones de reutilización.
Los lenguajes OOP de tipo estático utilizan tradicionalmente el subtipo nominal, lo que significa que un código diseñado para la clase / módulo / interfaz A solo puede tratar con la clase / módulo / interfaz B cuando B menciona explícitamente a A. Los idiomas en la familia de programación funcional utilizan principalmente subtipos estructurales, lo que significa ese código diseñado para A puede manejar B siempre que B tenga todos los métodos y / o campos de A. B podría haber sido creado por un equipo diferente antes de que fuera necesaria una clase / interfaz más general A. Por ejemplo, en OCaml, subtipo estructural se aplica al sistema de módulos, el sistema de objetos tipo OOP y sus tipos de variantes polimórficas bastante únicas.
La diferencia más destacada entre OOP y FP wrt. La modularidad es que la "unidad" predeterminada en OOP agrupa como un objeto varias operaciones en el mismo caso de valores, mientras que la "unidad" predeterminada en FP agrupa como una función de la misma operación para varios casos de valores. En FP todavía es muy fácil agrupar operaciones, por ejemplo, como módulos. (Por cierto, ni Haskell ni F # tienen un sistema completo de módulos de la familia ML). El problema de expresiónes la tarea de agregar de manera incremental tanto las nuevas operaciones que funcionan en todos los valores (por ejemplo, adjuntar un nuevo método a los objetos existentes) como los nuevos casos de valores que todas las operaciones deberían admitir (por ejemplo, agregar una nueva clase con la misma interfaz). Como se discutió en la primera conferencia de Ralf Laemmel a continuación (que tiene ejemplos extensos en C #), agregar nuevas operaciones es problemático en los lenguajes OOP.
La combinación de OOP y FP en Scala podría convertirlo en uno de los lenguajes más poderosos wrt. modularidad. Pero OCaml sigue siendo mi idioma favorito y, en mi opinión personal y subjetiva, no está lejos de Scala. Las dos conferencias de Ralf Laemmel a continuación discuten la solución al problema de expresión en Haskell. Creo que esta solución, aunque funciona perfectamente, dificulta el uso de los datos resultantes con polimorfismo paramétrico. Resolver el problema de expresión con variantes polimórficas en OCaml, explicado en el artículo de Jaques Garrigue vinculado a continuación, no tiene este inconveniente. También enlazo a capítulos de libros de texto que comparan los usos de la modularidad sin OOP y OOP en OCaml.
A continuación se encuentran los enlaces específicos de Haskell y OCaml que se expanden sobre el Problema de Expresión :
fuente
En realidad, el código OO es mucho menos reutilizable, y eso es por diseño. La idea detrás de OOP es restringir las operaciones en piezas de datos particulares a cierto código privilegiado que está en la clase o en el lugar apropiado en la jerarquía de herencia. Esto limita los efectos adversos de la mutabilidad. Si una estructura de datos cambia, solo hay algunos lugares en el código que pueden ser responsables.
Con la inmutabilidad, no le importa quién puede operar en una estructura de datos dada, porque nadie puede cambiar su copia de los datos. Esto facilita la creación de nuevas funciones para trabajar en estructuras de datos existentes. Simplemente cree las funciones y agrúpelas en módulos que parezcan apropiados desde el punto de vista del dominio. No tiene que preocuparse por dónde encajarlos en la jerarquía de herencia.
El otro tipo de reutilización de código es crear nuevas estructuras de datos para trabajar en funciones existentes. Esto se maneja en lenguajes funcionales usando características como genéricos y clases de tipos. Por ejemplo, la clase de tipo Ord de Haskell le permite usar la
sort
función en cualquier tipo con unaOrd
instancia. Las instancias son fáciles de crear si aún no existen.Tome su
Animal
ejemplo y considere implementar una función de alimentación. La implementación directa de OOP es mantener una colección deAnimal
objetos y recorrerlos todos, llamando alfeed
método en cada uno de ellos.Sin embargo, las cosas se ponen difíciles cuando te pones a detalles. Un
Animal
objeto, naturalmente, sabe qué tipo de comida come y cuánto necesita para sentirse lleno. No No sabe, naturalmente, donde se guarda la comida y la cantidad está disponible, por lo que unFoodStore
objeto ha pasado a ser una dependencia de todoAnimal
, ya sea como un campo delAnimal
objeto, o en el pasado como un parámetro delfeed
método. Alternativamente, para mantener laAnimal
clase más cohesionada, puede moversefeed(animal)
alFoodStore
objeto, o puede crear una abominación de una clase llamada unoAnimalFeeder
o algo así.En FP, no hay inclinación para que los campos de una
Animal
permanezcan siempre agrupados, lo que tiene algunas implicaciones interesantes para la reutilización. Digamos que tiene una lista deAnimal
registros, con campos comoname
,species
,location
,food type
,food amount
, etc También tiene una lista deFoodStore
registros con campos comolocation
,food type
, yfood amount
.El primer paso en la alimentación podría ser mapear cada una de esas listas de registros a listas de
(food amount, food type)
parejas, con números negativos para las cantidades de los animales. Luego puede crear funciones para hacer todo tipo de cosas con estos pares, como sumar las cantidades de cada tipo de alimento. Estas funciones no pertenecen a la perfección ya sea a unaAnimal
o unFoodStore
módulo, pero son altamente reutilizables por ambos.Terminas con un montón de funciones que hacen cosas útiles
[(Num A, Eq B)]
que son reutilizables y modulares, pero tienes problemas para averiguar dónde colocarlas o cómo llamarlas como grupo. El efecto es que los módulos FP son más difíciles de clasificar, pero la clasificación es mucho menos importante.fuente
Una de las soluciones populares es dividir el código en módulos, así es como se hace en JavaScript:
El artículo completo que explica este patrón en JavaScript , además de que hay varias otras formas de definir un módulo, como RequireJS , CommonJS , Google Closure. Otro ejemplo es Erlang, donde tiene módulos y comportamientos que imponen API y patrones, desempeñando un papel similar al de las interfaces en OOP.
fuente