API y programación funcional

15

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?

jameslk
fuente
2
Puede definir una interfaz formal sin la noción de objetos. Simplemente cree la función de la interfaz que los documenta. No proporcione documentación para detalles de implementación. Acaba de crear una interfaz.
Scara95
@ Scara95 ¿No significa eso que tengo que trabajar para implementar el código de una interfaz y escribir suficiente documentación al respecto para advertir al consumidor qué hacer y qué no hacer? ¿Qué sucede si el código cambia y la documentación se vuelve obsoleta? Generalmente prefiero el código de autodocumentación por este motivo.
jameslk
Tienes que documentar la interfaz de todos modos.
Scara95
33
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.
Doval

Respuestas:

10

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 instanceofconversió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:

  1. La inmutabilidad como la omnipresente omisión. Puede entregar valores de datos transparentes a código de terceros. No pueden modificarlos y ponerlos en estados no válidos. (La respuesta de Karl hace este punto).
  2. Sistemas de tipos sofisticados con tipos de datos algebraicos que le permiten controlar con precisión la estructura de sus tipos, sin escribir mucho código. Al usar juiciosamente estas instalaciones, a menudo puede diseñar tipos donde los "malos estados" son simplemente imposibles. (Eslogan: "Hacer que los estados ilegales sean irrepresentables". ) En lugar de utilizar la encapsulación para controlar indirectamente el conjunto de estados admisibles de una clase, ¡prefiero decirle al compilador cuáles son y hacer que me los garantice!
  3. Patrón de intérprete, como ya se mencionó. Una clave para diseñar un buen tipo de árbol de sintaxis abstracta es:
    • Intente y diseñe el tipo de datos del árbol de sintaxis abstracta para que todos los valores sean "válidos".
    • De lo contrario, haga que el intérprete detecte explícitamente combinaciones no válidas y las rechace limpiamente.

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!

sacundim
fuente
9

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.

Karl Bielefeldt
fuente
Una cosa que me gustaría agregar es que la variable inmutable hace que los programadores se adhieran a la estructura de datos distribuidos y dispersos, si es que hay una estructura. Todos los datos están estructurados para crear un grupo lógico, para facilitar el descubrimiento y el desplazamiento, no para el transporte. Esta es una progresión lógica que hará una vez que haya realizado suficiente programación funcional.
Xephon
8

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.

Sebastian Redl
fuente
Acerca de los lenguajes funcionales (puramente) tipados estáticamente, no existe una forma (simple) de llevar una función con un tipo de datos como en la programación OO. Entonces la documentación importa de todos modos. El punto es que no necesita soporte de idiomas para definir una interfaz.
Scara95
55
@ Scara95 ¿Puede explicar qué quiere decir con "llevar una función con un tipo de datos"?
Sebastian Redl
6

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.

Basile Starynkevitch
fuente