Table of Contents
La última semana de Mayo tuvo lugar en la bonita ciudad de Barcelona una feria anual para programadores, empresas de software y, en general, entusiastas de las Tecnologías de la Información – la JBCNConf, que este año celebró su quinta edición. Tres días con muchas ponencias y talleres (64 ponencias y 10 talleres, para ser más exactos) divididos en 5 tracks paralelos y con la participación de más de 80 especialistas.
Los temas tratados durante las ponencias fueron muy variados, pasando de charlas muy técnicas centradas en desvelar las particularidades y características de Java y Kotlin a través de operaciones de desarrollo avanzadas como deployments, analítica, auto-scaling y buenas prácticas de arquitectura bien establecidas, a otras más centradas en técnicas de productividad y organización del trabajo.
Obviamente, no nos podíamos perder este evento, y de hecho Apiumhub estuvo presente en todos los frentes: a nivel de sponsorización, en una de las charlas y cómo asistentes al evento.
Y hoy me gustaría compartir con vosotros varios apuntes derivados de la ponencia que más captó mi atención y posiblemente más me influyó de toda la conferencia. Me refiero a la presentación que dio Mario Fusco, una respetada figura dentro de la comunidad de programación; ponente, mentor, organizador de conferencias, escritor y devoto desarrollador en Red Hat.
En busca de la Programación Pragmática
El tema principal de su ponencia era: From object oriented to functional and back: the pursuit of pragmatic programming (“de orientada a objetos a funcional y de vuelta: en busca de la programación pragmática”). La premisa es simple: no luches contra el paradigma de Programación Orientada a Objetos ni te entregues en cuerpo y alma a la Programación Funcional; se inteligente, se pragmático y busca el equilibrio. En esta era de las fake news, los artículos clickbait y la confrontación constante parece que solamente existan las decisiones extremas, que el camino ponderado y centrado ha desaparecido. Como dice Mario, vivimos en un mundo de falsas dicotomías: blanco contra negro, bueno contra malo, Programación Orientada a Objetos contra Programación Funcional… vemos las cosas como opuestas y excluyentes. Y debemos elegir entre dos caminos supuestamente contradictorios sin posibilidad de acuerdo.
La razón puede ser que siempre buscamos soluciones sencillas, incluso podríamos decir que demasiado sencillas. Cómo si el limitarnos a solo la mitad de opciones potenciales pudiera de algún modo mejorar nuestras vidas. Y la realidad es que no existe necesidad alguna de elegir ni razón por la que descartar grandes cantidades de conocimiento, todo para seguir una división imaginaria. Cierto es que existen ocasiones en las que tiene sentido elegir una opción en vez de otra. Por ejemplo, no querrás empezar un proyecto usando dos lenguajes para tu backend o que se conecte a todas las bases de datos existentes. Pero si hablamos de estilo de código, este requerimiento no existe.
Programación Orientada a Objetos
La Programación Orientada a Objetos lleva ya décadas entre nosotros, aunque la idea original es incluso más antigua. Podemos datar la popularización del término en el año 1994 con la publicación del libro de Patrones de Diseño a cargo de “the Gang of Four”. Todos sabemos como preparar e implementar nuestras interfaces, tenemos nuestros factories, estrategias, decorators y facades bajo control… La idea original de la Programación Orientada a Objetos no era la de estar “orientada a clases”, que es como la usamos hoy en día, sino “orientada a mensajes” (entre estos objetos) mediante el patrón “actor model”. Pero ya es una forma de trabajar totalmente asentada y no la vamos a cambiar, nos guste o no. Funciona suficientemente bien y soluciona problemas (aunque también crea de nuevos). ¿Nos ofrece la Programación Funcional una solución sin este coste?
Programación Funcional
No es fácil describir qué es exactamente la Programación Funcional. Tiene algo que ver con mónadas e inmutabilidad… ¿Pero es realmente superior? Ya que todos los lenguajes modernos ofrecen soporte para funciones como first class citizens, ¿son todos funcionales?
Lo más sencillo sería decir que, si la Programación Orientada a Objetos es un objeto con información y una interfaz pública que nos permita manipularla, la Programación Funcional sería un grupo de funciones componibles operando en los modelos de información inmutable. Desacoplar el modelo del comportamiento, asegurarse que la información es inmutable y las funciones puras, y ya estaría. Podríamos incluso decir que tampoco difiere tanto un modelo del otro. Desde la perspectiva del programador, es sencillamente una notación inversa: en vez de decir “shoppingList.sum()” decimos “sum(shoppingList)”.
Polimorfismo
El factor diferencial entre la Programación Funcional y la Programación Orientada a Objetos es el polimorfismo. La posibilidad de asignar un comportamiento a los objetos y no crear una dependencia entre invocador e invocado. Como sabemos, existen dos cara de la computación: en un lado está la información y en el otro el comportamiento. La Programación Orientada a Objetos crea de forma natural una interfaz que define claramente qué comportamiento deseamos. Cada clase que implementa la interfaz es independiente y encapsula su lógica de forma interna.
Ejemplo (muy) simplificado:
public interface Shape {
double getArea();
double getCircumference();
}
public class Rectangular implements Shape {
private final int a;
private final int b;
public Rectangular(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public double getArea() { return a * b; }
@Override
public double getCircumference() { return 2 * (a + b); }
}
public class Circle implements Shape {
private final int r;
public Circle(int r) {
this.r = r;
}
@Override
public double getArea() {
return Math.PI * r * r;
}
@Override
public double getCircumference() {
return 2 * Math.PI * r;
}
}
Por otro lado, en Programación Funcional definimos los data objects sin comportamiento y las funciones que son a su vez el comportamiento requieren saber toda la variedad de implementaciones para cada posible rama. Acopla el comportamiento a la información, ya que las funciones deben conocer todos los data models posibles que habrá que tratar.
public interface Shape { }
public class Rectangular implements Shape {
public final int a;
public final int b;
public Rectangular(int a, int b) {
this.a = a;
this.b = b;
}
}
public class Circle implements Shape {
public final int r;
public Circle(int r) {
this.r = r;
}
}
public static double getCircumference(Shape shape) {
if (shape instanceof Circle) {
return 2 * Math.PI * ((Circle) shape).r;
}
if (shape instanceof Rectangular) {
return 2 * (((Rectangular) shape).a + ((Rectangular) shape).b);
}
throw new NotImplementedException();
}
public static double getArea(Shape shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).r * ((Circle) shape).r;
}
if (shape instanceof Rectangular) {
return ((Rectangular) shape).a * ((Rectangular) shape).b;
}
throw new NotImplementedException();
}
Las funciones que definen el comportamiento dependen de todas las implementaciones de la interfaz común. Aquí pues tenemos una pequeña victoria para la Programación Orientada a Objetos, aunque no debemos olvidar que es un caso particular en que el polimorfismo funciona mejor. Podemos encontrar ejemplos que se beneficiarían de un acercamiento funcional. La lección que hay que aprender de todo esto es la siguiente: debemos usar la herramienta adecuada para solucionar cada problema.
Más definiciones
Más adelante, Mario analizó de forma más detallada qué nos ofrece cada paradigma, siempre bajo esta perspectiva de programación pragmática que quiso inculcar entre los asistentes. Centrémonos momentáneamente en algunas definiciones:
Composición – encontraremos el concepto de composición en ambos mundos, pero en cada uno significa algo distinto. Composición es una propiedad de funciones, usada frecuentemente en Programación Funcional y que toma prestados elementos de las matemáticas, ya que dos funciones se consideran componibles cuando:
f : X → Y and g : Y → Z if (g ∘ f )(x) = g(f(x))
O en palabras más simples, podemos “pegar dos elementos y obtener un tercero con las propiedades exactas”. En el mundo de la Programación Orientada a Objetos, la composición se usa en el sentido de componer objetos en asociaciones más sofisticadas, normalmente con la ayuda de algún mecanismo de inyección de dependencias, lo cual puede ser una bendición o una maldición…
Inmutabilidad – Un elemento muy importante de la Programación Funcional, nos permite simplificar o incluso evitar muchos de los problemas del diseño orientado a objetos. Nos permite usar el paralelismo en un ámbito thread safe sin que puedan aparecer race conditions y no sea necesaria la sincronización. Es más fácil cachear, así como lograr una mayor consistencia y corrección de la información. Además, proporciona una mejor encapsulación. Como desventaja está quizás un rendimiento más bajo debido a la constante recreación de la totalidad de los data objects, pero hay trucos para solucionar este problema.
Gestión de los errores – La Programación Funcional popularizó la declaración monádica de retorno. Es natural en Programación Orientada a Objetos lanzar un error (si, por ejemplo, se ha detectado una división entre 0). En Programación Funcional se nos ofrece la posibilidad de volver un optional, el cual tiene varios beneficios – se puede memorizar y no rompe el flow de ejecución, por ejemplo. Según Fusco, las excepciones se deberían evitar y solo ser lanzadas como última opción por errores no recuperables. Para todos los demás casos, el equivalente más claro sería el de un salto GOTO.
Programación Imperativa vs Declarativa – Una pequeña función declarativa es en la mayoría de casos más concisa, limpia y fácil de leer que unos for-loops desorganizados con sus pointers y checks. ¿Es mucho más atractivo, no es cierto? Pero no nos olvidemos que esta belleza superficial y esta sencillez no aguantan especialmente el tipo cuando llega una petición de cambio. El viejo método imperativo aguanta mucho mejor estas peticiones, mientras que el método declarativo muchas veces nos obliga a reconstruir desde cero.
Un ejemplo muy ilustrativo que dio Mario durante su ponencia: imagina que tienes un fichero de logs con un montón de entradas, y mira de extraer las últimas 40 líneas que contengan un “error”. Hazlo siguiente las dos metodologías, imperativa y declarativa. Y ahora, ¿qué deberías hacer para no coger únicamente la línea que contiene el error, sino también la que la precede?
Resumen de la ponencia sobre Programación Pragmática
Ambos estilos, Orientado a Objetos y Funcional, nos ofrecen acercamientos muy distintos a la programación. Mientras el primero nos ofrece el polimorfismo, el segundo nos da la descomposición funcional. Por definición, la Programación Orientada a Objetos es stateful, mutable e imperativa en su estilo, mientras que la Programación Funcional ofrece statelessness, inmutabilidad y un estilo declarativo. Más ítems de la Programación Orientada a Objetos: excepciones, threads, statements e iteraciones; mientras que para la Programación Funcional tenemos optionals, validations, futures, expressions y recursion.
Al final, la lección más importante es que debemos conocer y entender ambos mundos, tener fluidez con ambos estilos y ser capaz de intercambiarlos cuando sea preciso. La clave para un buen código es evitar los extremos – probablemente no necesites ser 100% funcional, pero tampoco deberías evitar ese estilo a toda costa. Busca el equilibrio con el que te sientas mejor, bien sea con un diseño orientado a objetos bien establecido con algunas partes funcionales o un esqueleto funcional manipulando algunos objetos stateful. Quédate con lo mejor de cada mundo, evita los extremos, se inteligente, se pragmático.
¡Muchas gracias a Mario Fusco por su gran trabajo!
More to Explore