No hace mucho tiempo, aparecieron varias publicaciones en Haber que contrastaban el enfoque funcional y de objeto, lo que generó en los comentarios una acalorada discusión sobre lo que realmente es: la programación orientada a objetos y cómo difiere de lo funcional. Yo, aunque con algo de retraso, quiero compartir con otros lo que Robert Martin, también conocido como tío Bob, piensa sobre esto.
En los últimos años, repetidamente he podido programar en conjunto con personas que estudian Programación Funcional y que están sesgadas acerca de la POO. Esto generalmente se expresaba en forma de declaraciones como: "Bueno, esto se parece demasiado a algo objeto".
Creo que esto proviene de la creencia de que FP y OOP son mutuamente excluyentes. Muchos parecen pensar que si el programa es funcional, entonces no está orientado a objetos. Creo que la formación de tal opinión es una consecuencia lógica del estudio de algo nuevo.
Cuando adoptamos una nueva técnica, a menudo comenzamos a evitar las técnicas antiguas que utilizamos antes. Esto es natural, porque creemos que la nueva técnica es "mejor" y, por lo tanto, la técnica anterior es probablemente "peor".
En esta publicación, estoy justificado en la opinión de que si bien OOP y FP son ortogonales, estos no son conceptos mutuamente excluyentes. Que un buen programa funcional puede (y debe) estar orientado a objetos. Y que un buen programa orientado a objetos puede (y debería) ser funcional. Pero para hacer esto, tenemos que determinar los términos.
¿Qué es la POO?
Abordaré el tema desde una perspectiva reduccionista. Hay muchas definiciones correctas de OOP que cubren muchos conceptos, principios, técnicas, patrones y filosofías. Tengo la intención de ignorarlos y centrarme en la sal misma. Aquí, el reduccionismo es necesario porque toda esta riqueza de oportunidades en torno a la POO no es realmente algo específico de la POO; es solo parte de la gran cantidad de oportunidades que se encuentran en el desarrollo de software en general. Aquí me centraré en la parte de OOP, que es definitoria e inamovible.
Mira dos expresiones:
1: f (o); 2: de ();
Cual es la diferencia
Claramente no hay diferencia semántica. Toda la diferencia está completamente en la sintaxis. Pero uno parece de procedimiento y el otro está orientado a objetos. Esto se debe a que estamos acostumbrados al hecho de que para la expresión 2. implica implícitamente una semántica de comportamiento especial que no tiene la expresión 1. Esta semántica de comportamiento particular es el polimorfismo.
Cuando vemos la expresión 1. vemos la función f , que se llama a la cual se transfiere el objeto o . Esto implica que solo hay una función llamada f, y no el hecho de que sea miembro de la cohorte estándar de funciones que rodean a o.
Por otro lado, cuando vemos la expresión 2. vemos un objeto con el nombre o al que se envía un mensaje con el nombre f . Esperamos que pueda haber otros tipos de objetos que reciben el mensaje f, y por lo tanto no sabemos qué comportamiento específico esperar de f después de la llamada. El comportamiento depende del tipo o. es decir, f es polimórfico.
Este hecho que esperamos de los métodos de comportamiento polimórfico es la esencia de la programación orientada a objetos. Esta es una definición reduccionista y esta propiedad no puede eliminarse de OOP. OOP sin polimorfismo no es OOP. Todas las demás propiedades de OOP, como la encapsulación de datos y los métodos vinculados a estos datos e incluso la herencia, están más relacionados con la expresión 1. que con la expresión 2.
Los programadores que usan C y Pascal (y hasta cierto punto incluso Fortran y Kobol) siempre han creado sistemas de funciones y estructuras encapsuladas. Para crear tales estructuras, ni siquiera necesita un lenguaje de programación orientado a objetos. La encapsulación e incluso la herencia simple en tales lenguajes es obvia y natural. (En C y Pascal más naturalmente que en otros)
Por lo tanto, lo que realmente distingue a los programas OOP de los que no lo son es el polimorfismo.
Es posible que desee argumentar que el poliforismo se puede hacer simplemente usando el interruptor f interno o largas cadenas if / else. Esto es cierto, así que necesito establecer otra limitación para OOP.
El uso del polimorfismo no debería crear la dependencia de la persona que llama con respecto a la llamada.
Para explicar esto, veamos nuevamente las expresiones. La expresión 1: f (o) parece depender de la función f en el nivel del código fuente. Llegamos a esta conclusión porque también suponemos que f es solo uno y que, por lo tanto, la persona que llama debe saber sobre la persona que llama.
Sin embargo, cuando miramos la Expresión 2. de () asumimos algo más. Sabemos que puede haber muchas realizaciones de f y no sabemos cuál de estas funciones se llamará realmente f. Por lo tanto, el código fuente que contiene la expresión 2 es independiente de la función que se llama en el nivel del código fuente.
Más específicamente, esto significa que los módulos (archivos con código fuente) que contienen llamadas a funciones polimórficas no deberían referirse a módulos (archivos con código fuente) que contienen la implementación de estas funciones. No puede haber inclusión, uso, requerimiento o cualquier otra palabra clave que haga que algunos archivos de código fuente dependan de otros.
Entonces, nuestra definición reduccionista de OOP es:
Una técnica que utiliza polimorfismo dinámico para llamar a funciones y no crea dependencias de la persona que llama en el llamado en el nivel del código fuente.
¿Qué es la FA?
Y nuevamente, utilizaré el enfoque reduccionista. FP tiene ricas tradiciones e historia, cuyas raíces son más profundas que la programación misma. Hay principios, técnicas, teoremas, filosofías y conceptos que impregnan este paradigma. Ignoraré todo esto y pasaré directamente a la esencia, a la propiedad inherente que separa a FP de otros estilos. Aquí esta:
f (a) == f (b) si a == b.
En un programa funcional, llamar a una función con el mismo argumento da el mismo resultado sin importar cuánto tiempo haya estado ejecutándose el programa. Esto a veces se llama transparencia referencial.
De lo anterior se deduce que f no debería cambiar las partes del estado global que afectan el comportamiento de f. Además, si decimos que f representa todas las funciones en el sistema, es decir, todas las funciones en el sistema deben ser referencialmente transparentes, entonces ninguna función en el sistema puede cambiar el estado global. Ninguna función puede hacer algo que pueda llevar a que otra función del sistema devuelva un valor diferente con los mismos argumentos.
Esto tiene una consecuencia más profunda: no se puede cambiar ningún valor con nombre. Es decir, no hay operador de asignación.
Si considera cuidadosamente esta afirmación, puede llegar a la conclusión de que un programa que consta de funciones transparentes y transparentes no puede hacer nada, ya que cualquier comportamiento útil del sistema cambia el estado de algo; incluso si es solo el estado de la impresora o pantalla. Sin embargo, si excluimos el hierro de los requisitos de transparencia referencial y de todos los elementos del mundo que nos rodea, resulta que podemos crear sistemas muy útiles.
El enfoque, por supuesto, está en la recursividad. Considere una función que toma una estructura con estado como argumento. Este argumento consiste en toda la información de estado que una función necesita para funcionar. Cuando finaliza el trabajo, la función crea una nueva estructura con un estado cuyo contenido es diferente del anterior. Y con la última acción, la función se llama a sí misma con una nueva estructura como argumento.
Este es solo uno de los trucos simples que un programa funcional puede usar para almacenar cambios de estado sin tener que cambiar el estado [1].
Entonces, la definición reduccionista de la programación funcional:
Transparencia referencial: no puede reasignar valores.
FP vs OOP
En este punto, los defensores de OOP y los defensores de FI ya me están mirando a través de miras ópticas. El reduccionismo no es la mejor manera de hacer amigos. Pero a veces es útil. En este caso, creo que es útil arrojar luz sobre el holivar anti-OOP sin fin.
Está claro que las dos definiciones reduccionistas que he elegido son completamente ortogonales. El polimorfismo y la transparencia referencial no tienen nada que ver entre sí. No se cruzan de ninguna manera.
Pero la ortogonalidad no implica exclusión mutua (pregunte a James Clerk Maxwell). Es completamente posible crear un sistema que utilice tanto el polimorfismo dinámico como la transparencia referencial. ¡No solo es posible, es correcto y bueno!
¿Por qué es buena esta combinación? ¡Por exactamente las mismas razones que sus dos componentes! Los sistemas construidos sobre polimorfismo dinámico son buenos porque tienen baja conectividad. Las dependencias se pueden invertir y colocar en diferentes lados de los límites arquitectónicos. Estos sistemas se pueden probar utilizando Moki y Fake y otros tipos de Dobles de prueba. Los módulos se pueden modificar sin realizar cambios en otros módulos. Por lo tanto, tales sistemas son más fáciles de modificar y mejorar.
Los sistemas basados en la transparencia referencial también son buenos porque son predecibles. La inmutabilidad del estado hace que tales sistemas sean más fáciles de entender, cambiar y mejorar. Esto reduce en gran medida la probabilidad de carreras y otros problemas de subprocesos múltiples.
La idea principal aquí es esta:
No hay holivar FP vs OOP
FP y OOP funcionan bien juntos. Ambos son buenos y adecuados para usar en sistemas modernos. El sistema, que se basa en una combinación de los principios de OOP y FP, maximiza la flexibilidad, el mantenimiento, la capacidad de prueba, la simplicidad y la resistencia. Si elimina uno para agregar otro, solo empeorará la estructura del sistema.
[1] Dado que utilizamos máquinas con arquitectura Von Neumann, suponemos que tienen celdas de memoria cuyo estado realmente cambia. En el mecanismo de recursión que describí, la optimización de la recursión de la cola no permitirá la creación de nuevos marcos de cristal y se utilizará el marco de cristal original. Pero esta violación de la transparencia referencial (generalmente) está oculta para el programador y no afecta nada.