Desde mi exposición (ciertamente limitada) a lenguajes de programación funcionales, como Clojure, parece que la encapsulación de datos tiene un papel menos importante. Por lo general, varios tipos nativos, como mapas o conjuntos, son la moneda preferida para representar datos sobre los objetos. Además, esos datos son generalmente inmutables.
Por ejemplo, aquí está una de las citas más famosas de Rich Hickey de la fama Clojure, en una entrevista sobre el asunto :
Fogus: Siguiendo esa idea, algunas personas se sorprenden por el hecho de que Clojure no participa en la encapsulación de ocultación de datos en sus tipos. ¿Por qué decidiste renunciar a ocultar datos?
Hickey: Seamos claros, Clojure enfatiza fuertemente la programación a las abstracciones. Sin embargo, en algún momento, alguien necesitará tener acceso a los datos. Y si tiene una noción de "privado", necesita las nociones correspondientes de privilegio y confianza. Y eso agrega un montón de complejidad y poco valor, crea rigidez en un sistema y a menudo obliga a las cosas a vivir en lugares que no deberían. Esto se suma a la otra pérdida que ocurre cuando la información simple se coloca en clases. En la medida en que los datos sean inmutables, hay poco daño que puede resultar de proporcionar acceso, aparte de que alguien podría llegar a depender de algo que podría cambiar. Bueno, está bien, la gente hace eso todo el tiempo en la vida real, y cuando las cosas cambian, se adaptan. Y si son racionales, saben cuándo toman una decisión basada en algo que puede cambiar y que en el futuro podrían necesitar adaptarse. Por lo tanto, es una decisión de gestión de riesgos, una que creo que los programadores deberían tener libertad para tomar. Si las personas no tienen la sensibilidad para desear programar en abstracciones y desconfiar de casarse con los detalles de implementación, entonces nunca serán buenos programadores.
Viniendo del mundo OO, esto parece complicar algunos de los principios consagrados que he aprendido a lo largo de los años. Estos incluyen información oculta, la ley de Demeter y el principio de acceso uniforme, por nombrar algunos. El hilo común es que la encapsulación nos permite definir una API para que otros sepan qué deben y qué no deben tocar. En esencia, crear un contrato que permita al responsable del mantenimiento de algunos códigos realizar libremente cambios y refactorizaciones sin preocuparse de cómo podría introducir errores en el código del consumidor (principio abierto / cerrado). También proporciona una interfaz limpia y curada para que otros programadores sepan qué herramientas pueden usar para obtener o aprovechar esos datos.
Cuando se permite acceder directamente a los datos, ese contrato de API se rompe y todos esos beneficios de encapsulación parecen desaparecer. Además, los datos estrictamente inmutables parecen hacer que pasar estructuras específicas de dominio (objetos, estructuras, registros) sea mucho menos útil en el sentido de representar un estado y el conjunto de acciones que se pueden realizar en ese estado.
¿Cómo abordan las bases de código funcionales estos problemas que parecen surgir cuando el tamaño de una base de código crece enormemente, de modo que las API deben definirse y muchos desarrolladores participan en el trabajo con partes específicas del sistema? ¿Hay ejemplos de esta situación disponibles que demuestren cómo se maneja esto en este tipo de bases de código?
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.
Realmente no. Lo único que cambia es que los cambios terminan en un nuevo objeto. Esta es una gran victoria cuando se trata de razonar sobre el código; pasar objetos mutables significa tener que hacer un seguimiento de quién podría mutarlos, un problema que aumenta con el tamaño del código.Respuestas:
En primer lugar, voy a comentar en segundo lugar los comentarios de Sebastian sobre lo que es funcionalmente adecuado, lo que es la escritura dinámica. Más generalmente, Clojure es un sabor de lenguaje funcional y comunidad, y no debe generalizar demasiado en función de él. Haré algunos comentarios desde una perspectiva más de ML / Haskell.
Como menciona Basile, el concepto de control de acceso existe en ML / Haskell, y a menudo se usa. El "factoring" es un poco diferente de los lenguajes OOP convencionales; en OOP, el concepto de clase juega simultáneamente el papel de tipo y módulo , mientras que los lenguajes funcionales (y de procedimiento tradicional) los tratan ortogonalmente.
Otro punto es que ML / Haskell son muy pesados en genéricos con borrado de tipo, y que esto puede usarse para proporcionar un sabor diferente de "ocultación de información" que la encapsulación OOP. Cuando un componente solo conoce el tipo de un elemento de datos como parámetro de tipo, ese componente puede recibir valores de ese tipo de forma segura y, sin embargo, se evitará que haga mucho con ellos porque no sabe y no puede conocer su tipo concreto (no hay
instanceof
conversión universal o de tiempo de ejecución en estos idiomas). Esta entrada de blog es uno de mis ejemplos introductorios favoritos de estas técnicas.A continuación: en el mundo FP es muy común usar estructuras de datos transparentes como interfaces para componentes opacos / encapsulados. Por ejemplo, los patrones de intérprete son muy comunes en FP, donde las estructuras de datos se usan como árboles de sintaxis que describen la lógica y se alimentan al código que las "ejecuta". El estado, propiamente dicho, existe efímeramente cuando se ejecuta el intérprete que consume las estructuras de datos. Además, la implementación del intérprete puede cambiar siempre que se comunique con los clientes en términos de los mismos tipos de datos.
Último y más largo: la encapsulación / ocultación de información es una técnica , no un fin. Pensemos un poco en lo que proporciona. La encapsulación es una técnica para conciliar el contrato y la implementación de una unidad de software. La situación típica es esta: la implementación del sistema admite valores o establece que, de acuerdo con su contrato, no debería existir.
Una vez que lo mire de esta manera, podemos señalar que FP proporciona, además de la encapsulación, una serie de herramientas adicionales que se pueden utilizar para el mismo fin:
Esta serie F # "Diseñando con tipos" es una lectura bastante decente sobre algunos de estos temas, particularmente el # 2. (Es de donde proviene el enlace "hacer que los estados ilegales sean irrepresentables" desde arriba.) Si observa de cerca, notará que en la segunda parte demuestran cómo usar la encapsulación para ocultar constructores y evitar que los clientes construyan instancias no válidas. Como dije anteriormente, ¡ es parte del conjunto de herramientas!
fuente
Realmente no puedo exagerar el grado en que la mutabilidad causa problemas en el software. Muchas de las prácticas que se nos pasan por la cabeza compensan los problemas que causa la mutabilidad. Cuando eliminas la mutabilidad, no necesitas esas prácticas tanto.
Cuando tiene inmutabilidad, sabe que su estructura de datos no cambiará inesperadamente debajo de usted durante el tiempo de ejecución, por lo que puede crear sus propias estructuras de datos derivados para su propio uso a medida que agrega características a su programa. La estructura de datos original no necesita saber nada sobre estas estructuras de datos derivados.
Esto significa que sus estructuras de datos base tienden a ser extremadamente estables. Las nuevas estructuras de datos se derivan de él alrededor de los bordes según sea necesario. Es realmente difícil de explicar hasta que hayas realizado un programa funcional significativo. Simplemente te preocupa cada vez menos la privacidad y piensas en crear estructuras de datos públicos genéricos duraderos cada vez más.
fuente
La tendencia de Clojure a usar solo hashes y primitivas no es, en mi opinión, parte de su herencia funcional, sino parte de su herencia dinámica. He visto tendencias similares en Python y Ruby (ambas orientadas a objetos, imperativas y dinámicas, aunque ambas tienen un soporte bastante bueno para funciones de orden superior), pero no en, por ejemplo, Haskell (que está estáticamente tipado, pero es puramente funcional , con construcciones especiales necesarias para escapar de la inmutabilidad).
Entonces, la pregunta que debe hacer no es cómo manejan los lenguajes funcionales grandes API, sino cómo lo hacen los lenguajes dinámicos. La respuesta es: buena documentación y montones y montones de pruebas unitarias. Afortunadamente, los lenguajes dinámicos modernos generalmente vienen con muy buen soporte para ambos; por ejemplo, tanto Python como Clojure tienen una forma de incorporar documentación en el código en sí, no solo comentarios.
fuente
Algunos lenguajes funcionales brindan la capacidad de encapsular u ocultar detalles de implementación en módulos y tipos de datos abstractos .
Por ejemplo, OCaml tiene módulos definidos por una colección de tipos y valores abstractos con nombre (especialmente funciones que operan en estos tipos abstractos). Entonces, en cierto sentido, los módulos de Ocaml son API de referencia. Ocaml también tiene functores, que están transformando algunos módulos en otro, proporcionando así una programación genérica. Entonces los módulos son compositivos.
fuente