¿Cómo manejan los lenguajes puramente funcionales la modularidad?

23

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.

Cafe electrico
fuente
1
¿Por qué crees que funcional no significa clases? El algunas de las clases Frist vinieron de LISP - CLOS Mira clojure espacios de nombres y tipos o módulos y Haskell .
Me referí a los lenguajes funcionales que NO tienen clases, soy muy consciente de los pocos que HACEN
Electric Coffee
1
Caml como ejemplo, su lenguaje hermano OCaml agrega objetos, pero Caml en sí no los tiene.
Café eléctrico
12
El término "puramente funcional" se refiere a lenguajes funcionales que mantienen una transparencia referencial y no está relacionado con si el lenguaje tiene o no características orientadas a objetos.
sepp2k
2
El pastel es una mentira, la reutilización de código en OO es mucho más difícil que en FP. Por todo lo que OO ha reclamado la reutilización del código a lo largo de los años, lo he visto seguir un mínimo de veces. (siéntase libre de decir que debo estar haciéndolo mal, me siento cómodo con lo bien que escribo el código OO habiendo tenido que diseñar y mantener sistemas OO durante años, sé la calidad de mis propios resultados)
Jimmy Hoffa

Respuestas:

19

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.

Jörg W Mittag
fuente
8
¿Puedes explicar cómo funcionan esas cosas con un poco más de detalle en relación con oop? En lugar de simplemente afirmar que los conceptos existen ...
Electric Coffee
Nota al margen: los módulos y las unidades son dos construcciones diferentes en Racket: los módulos son comparables a los espacios de nombres y las unidades están a medio camino entre los espacios de nombres y las interfaces OO. Los documentos entran en mucho más detalles sobre las diferencias
Jack
@ Jack: No sabía que Racket también tiene un concepto llamado module. Creo que es lamentable que Racket tenga un concepto llamado moduleque no es un módulo, y un concepto que es un módulo pero no llamado module. 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?
Jörg W Mittag
Los módulos y las unidades son ambos grupos de nombres vinculados a valores. Los módulos pueden tener dependencias en otros conjuntos específicos de enlaces, mientras que las unidades pueden tener dependencias en algún conjunto general de enlaces que cualquier otro código que use la unidad tiene que proporcionar. Las unidades se parametrizan sobre enlaces, los módulos no. Un módulo dependiente de un enlace mapy una unidad dependiente de un enlace mapson diferentes en el sentido de que el módulo debe referirse a algún mapenlace específico , como el de racket/base, mientras que diferentes usuarios de la unidad pueden dar diferentes definiciones de mapla unidad.
Jack
4

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.

dan_waterworth
fuente
4

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 :

lukstafi
fuente
2
¿Le importaría explicar más sobre lo que hacen estos recursos y por qué los recomienda como respuesta a la pregunta que se hace? Las "respuestas de solo enlace" no son bienvenidas en Stack Exchange
mosto
2
Acabo de proporcionar una respuesta real en lugar de solo enlaces, como una edición.
lukstafi
0

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 sortfunción en cualquier tipo con una Ordinstancia. Las instancias son fáciles de crear si aún no existen.

Tome su Animalejemplo y considere implementar una función de alimentación. La implementación directa de OOP es mantener una colección de Animalobjetos y recorrerlos todos, llamando al feedmétodo en cada uno de ellos.

Sin embargo, las cosas se ponen difíciles cuando te pones a detalles. Un Animalobjeto, 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 un FoodStoreobjeto ha pasado a ser una dependencia de todo Animal, ya sea como un campo del Animalobjeto, o en el pasado como un parámetro del feedmétodo. Alternativamente, para mantener la Animalclase más cohesionada, puede moverse feed(animal)al FoodStoreobjeto, o puede crear una abominación de una clase llamada uno AnimalFeedero algo así.

En FP, no hay inclinación para que los campos de una Animalpermanezcan siempre agrupados, lo que tiene algunas implicaciones interesantes para la reutilización. Digamos que tiene una lista de Animalregistros, con campos como name, species, location, food type, food amount, etc También tiene una lista de FoodStoreregistros con campos como location, food type, y food 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 una Animalo un FoodStoremó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.

Karl Bielefeldt
fuente
-1

Una de las soluciones populares es dividir el código en módulos, así es como se hace en JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

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.

David Sergey
fuente