Composicion funcional

Composición funcional en javascript.

La programación funcional dispone de numerosas técnicas bien documentadas para trabajar con funciones, ya hemos visto unas cuantas en artículos previos en este blog, hoy le toca el turno a la composición funcional, una de las herramientas mas poderosas de la programación funcional.

Recapitulando.

Nosotros sabemos bien como generar una función en javascript, por ejemplo:

1
2
3
function saludar(name) {
return "Hola " + name;
}

¿Simple no?, y seguramente todos sabemos como ejecutar esta función:

1
saludar("gustabo"); // ==> Hola gustabo

Si lo admito, esta función es bastante aburrida por si misma, vamos a mostrar ese saludo por la consola:

1
console.log(saludar("gustabo"));

funciona, vamos a saludar a pedrito y ya que estamos (y para no repetir código), vamos a crear una función que nos abstraiga de tener que escribir ese console.log en todos lados. :

1
2
3
4
5
6
7
8
9
10
function saludar(name){
return "Hola " + name;
}
function mostrarSaludo (name) {
console.log(saludar(name));
}
mostrarSaludo("martin");
mostrarSaludo("pepito");

Y obviamente funciona, pero vamos un poco mas allá y mostremos nuestros saludos en un alert:

1
2
3
function alertSaludo (name) {
alert(saludar(name));
}

mmm algo no esta bien, ¡hemos tenido que crear 2 funciones para saludar por distintos medios a una persona!, eso no parece muy re utilizable y optimizado. ¿Porque ha pasado esto?, pues por la forma de componer las funciones que tiene el paradigma imperativo, function1( function2 ( function3 (argumentos...)));

Como bien sabemos, básicamente lo que estamos haciendo es una tubería con nuestros datos (un pipe para los usuarios de unix y derivados), el resultado de aplicar la funcion3 se lo pasamos como argumento de entrada a nuestra funcion2 y el resultado se lo pasamos como argumento de entrada a la funcion1. Esta forma de “combinar funciones” si bien es muy utilizada no es la única ni tampoco la mas eficiente ya que como vemos nos obliga a tener que crear toda una nueva función cada vez que algo cambia como en el ejemplo anterior.

Presentando a compose

Vamos a abstraer esta forma de componer nuestras funciones y vamos a darle un nombre ¿que tal compose?.

1
2
3
4
5
function compose(f,g) {
return function (x) {
return f(g(x));
}
}

Compose se lee de adentro hacia afuera, en este caso x es el argumento y g y f son las funciones que queremos componer juntas. Vamos a probar nuestro nuevo invento y a re definir nuestras amables funciones de saludos.

1
2
3
4
5
6
7
8
var saludarPorConsola = compose(console.log, saludar);
var saludarPorAlert = compose(alert, saludar);
saludarPorConsola("martin");
saludarPorConsola("pepito");
saludarPorAlert("martin");
saludarPorAlert("pepito");

Fantástico! hemos logrado reducir bastante la duplicidad del código y hasta hemos mejorado la legibilidad!. Es mas sencillo y cómodo tanto para generar nuestras funciones, como para leer y saber de un vistazo que es lo que hacen.

Básicamente esto se lee como:

compose(console.log, saludar); Es la composición de saludar con un console.log.

Nosotros solamente tenemos que tomar 2 funciones que nos interese combinar y componerlas juntas para obtener otra función, ¿genial no?. Esto tiene mucho sentido puesto que tomar 2 unidades de un mismo tipo nos debe de devolver una unidad de ese mismo tipo, en este caso una función. Si juntamos 2 ruedas no obtenemos una casa, hay una teoria matematica detras de esto y la veremos en breve. Ej:

1
2
3
4
5
1 + 2 == 3 // ==> number
"Hola " + "pepito" == "Hola pepito" // ==> string
compose(console.log, saludar) == function(x) { return console.log(saludar(x)); } // ==> function

Hagamos un par de ejercicios con nuestra flamante función compose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* ************* Funciones de utilidad ****************/
function head(vector) { return vector[0]; }
function uppercase (str) { return str.toUpperCase(); }
function exclaim (str) { return str + "!"; }
function reverse (lista) { return lista.reduce(function(acc, x) { return x.concat(acc); }); }
/****************************************************************************/
var last = compose(head, reverse);
var showLastName = compose(console.log, last);
var showLastNameExclaim = compose(console.log, compose(exclaim, last));
showLastName(["carlos", "martin", "gustavo"]); // ==> gustavo
showLastNameExclaim (["carlos", "martin", "gustavo"]); // ==> gustavo!
var addExclaim = compose(exclaim, exclaim);
var showLastNameAndAddExclaim = compose(console.log, compose(addExclaim,last));
showLastNameAndAddExclaim(["carlos", "martin", "gustavo"]); // ==> gustabo!!
var upperExclaimLast = compose(uppercase, compose(exclaim ,last));
var showUppername = compose(console.log, upperExclaimLast);
var alertUpperName = compose(alert, upperExclaimLast);

¿Que clase de brujera es esta?

Si si, así me quede yo cuando conocí esta técnica. Como vemos la reutilización de funciones y el principio DRI (don’t repeat yourself) se potencia al máximo al componer las funciones de esta manera. Ya que estamos creando nuevas funciones reutilizando funciones mas pequeñas y abstractas.

La función reverse invierte una lista y la función head toma el primer elemento de una lista, si lo combinamos juntos obtenemos una flamante función last con muy poco código.

Como vemos componer funciones es algo sencillo e intuitivo, aunque hay un par de puntos que aclarar.

  • El orden de composición si importa, en este ejemplo componemos nuestras funciones desde la derecha hacia la izquierda.

  • Las funciones a componer deberían de ser puras.

  • Las funciones a componer deberían de ser suficientemente abstractas como para solucionar una familia de problemas y no un problema en especifico.

Como la función compose viene directamente de las matemáticas, existe una propiedad que deberiamos conocer.

Asociatividad:

compose (f, compose( g, h)) es igual a compose (compose(f,g) , h );

Esto quiere decir que no importa como agrupemos nuestras llamadas a compose ya que el resultado siempre sera el mismo. Osea que si queremos capitalizar una cadena podemos hacer:

1
2
3
4
compose(uppercase, compose(head, reverse));
// o sino
compose(compose(uppercase, head), reverse);

El resultado es el mismo.

Existen librerias como ramda que tienen implementada la funcion compose con curring, lo que nos permite componer varias funciones juntas, con lo cual podriamos hacer:

1
compose(uppercase, head, reverse);

sin importar la cantidad de funciones juntas que deseemos componer (ventajas de la propiedad asociativa).

Pero de donde viene todo esto?

La teoría de categorías

En la teoría de categorías tenemos algo que se llama una categoría, la cual esta definida como una colección de cosas con los siguientes componentes:

  • Una colección de objetos

  • Una colección de morfismos

  • Una forma de componer los morfismos.

Vamos por partes.
una colección de objetos:
Los objetos los podemos ver como tipos de datos, como number, Boolean, String, Object etc etc.. Se suelen ver estos objetos como un conjunto de todos los posibles valores. Por ejemplo uno puede ver un String como un conjunto de todas las combinaciones alfanuméricas posibles, o un Bool como un conjunto de True False o number como un conjunto de todos los valores numéricos enteros posibles etc…, Tratar los tipos como conjuntos es útil porque podemos utilizar la teoría del conjunto con ellos.

Una colección de morfismos:
Los morfismos, no son mas que nuestras funciones puras de todos los días.

Una forma de componer en los morfismos

Esto como te daras cuenta, es nuestra función compose. Compose es asociativa puesto que es una propiedad necesaria para cualquier composición de la teoría categórica.

Comentarios