Programación Funcional en JavaScript: Los combinadores

17154466027_a6cfef91ed_k.jpg

Ponemos punto y final al bloque sobre modularidad y reusabilidad de código con este nuevo post. En esta ocasión vamos a hablar sobre un concepto muy ligado a la composición de programas por medio de funciones puras: los combinadores.

Los combinadores nacen de la lógica combinatoria y nos ayudan a orquestar todo el entramado de funciones que hemos ido creando. Una de las objeciones que se suele poner a la programación funcional es la dificultad para realizar ciertas tareas comunes que se suelen hacer de una manera bastante fácil en lenguajes imperativos. Los combinadores nos van a ayudar en estas tareas.

Los combinadores son funciones de orden superior sin lógica de negocio interna, simplemente son un mecanismos que permiten que podamos combinar artefactos, en este caso funciones u otros combinadores. Son las funciones que tienden puentes entre nuestros módulos o programas. Nos serán de gran ayuda para conseguir mayor expresividad en el código.

A lo largo del post, presentaremos los 5 combinadores más comunes en programación funcional. Como podréis ver si profundizáis en el tema, no serán los únicos, existen muchos más.

No me demoro. Os dejo con los combinadores:

Identidad (I-Combinator)

El combinador identidad es un combinador a simple vista muy tonto. Es un combinador que dada una función devuelve la misma función. Su forma normal es esta:

identity :: (a) -> a

Una forma de implementarla podría ser esta:

const identity = function (fn) {
    return fn;
}

Con esta implementación nos aporta poco, pero si la endulzamos un poquito, podemos ver que el combinador identidad nos puede ser útil:

const identity = function (fn) {
    return function (...args) {
        return fn.apply(this, args);
    };
}

La función combinadora se sigue comportando igual, pero nos ha dado la oportunidad de que nos sea útil. El combinador identidad puede ayudarnos a envolver una función.

De esta manera, hacemos que se ejecute algo antes o después de la ejecución de la función envuelta. Por ejemplo, nos puede ser útil para depurar funciones, para saber la entrada y la salida de una función.

Imaginemos que hacemos esto:

const identity = function (fn, logger) {
    return function (...args) {
        logger(args);
        const result = fn.apply(this, args);
        logger(result);
        return result;
    }
};

const exp = function (num) {
    return num * num;
};

const expLog = identity(exp, console.log);

Tap (K-Combinator)

El Combinador Tap (también conocido como K-Combinator o combinador constante) es el combinador que recibe un objeto y una función, ejecuta la función dada con el objeto dado como parámetro  y devuelve el mismo objeto que se pasó como parámetro.

Su notación formal es la siguiente:

tap :: (a -> *) -> a -> a

Nos es muy útil para poder ejecutar funciones que no devuelven ningún valor. Gracias a tap podemos ejecutarlas y hacer que el estado siga fluyendo en la tubería de funciones.

Un caso muy práctico es cuando queremos depurar o escribir logs en algún punto intermedio de la tubería. Volviendo al ejemplo del otro día, podríamos hacer algo de este estilo:

const debug = R.tap(console.log);
const getPartyWinner = R.pipe(
    R.zip,
    debug,
    R.sortBy(R.prop(1)),
    debug,
    R.reverse,
    debug,
    R.pluck(0),
    debug,
    R.head
);

De esta manera  veríamos qué objeto se está pasando a cada función. Es muy a tener en cuenta.

Alternancia (OR-Combinator)

El combinador alt nos permite incluir flujos condicionales en las aplicaciones funcionales. Este combinador toma dos funciones devuelve el valor de la primera si el resultado ofrecido no es false, null o undefined. Si no es así, devuelve el valor de la segunda.

Podemos implementar esta función de esta manera:

const alt = function (fn1, fn2) {
    return function (val) {
        return fn1(val) || fn2(val);
    }
}

También se puede definir de una forma más minimalista con currificación:

const alt = R.curry((fn1, fn2, val)  => fn1(val) || fn2(val));

Nos puede ser útil para comprobar si un elemento existe o para hacer una comprobación de nulls. Imaginemos un caso donde queremos mostrar el cliente encontrado en una base de datos y si no existe mostrar uno nuevo. En un estilo imperativo esto se desarrollaría de esta manera:

var client = findClient('444-444-444');
if (!client) {
   client = createClient('444-444-444');
}
append("#client-info', client);

De forma funcional y con el combinador alt esto quedaría así:

const showClient = R.pipe(
    alt(findClient, createClient),
    append('#client-info')
);
showClient('444-444-444');

Secuencia (S-Combinator)

El combinador secuencia se usa para iterar sobre una secuencia de funciones.  El combinador toma dos o más funciones como parámetro y devuelve una nueva función que ejecuta todas estas de forma secuencial. La secuencia se ejecuta contra el mismo valor. De esta forma su implementación es la siguiente:

const seq = function () {
    const funcs = Array.prototype.slice.call(arguments);
    return function (val) {
        funcs.forEach(function (fn) {
            fn(val)
        });
    }
}

Con esto y el ejemplo anterior, podríamos hacer cosas así:

const showClient = R.pipe(
    findClient,
    seq(
         append('#client-info'),
         console.log
    )  
);
showClient('444-444-444');

El combinador ‘seq’ no devuelve un valor al terminar su ejecución. Si quisiéramos que devolviese el valor sobre el que se itera la secuencia, podemos combinarlo con el combinador tap que hemos visto anteriormente.

Bifurcación-Unión (FJ-Combinator)

El combinador bifurcación (o Fork) nos viene muy bien cuando tenemos casos donde necesitamos procesar un simple recurso en dos diferentes formas y necesitamos combinar los resultados. Este combinador acepta tres funciones: 2 funciones que procesan el recurso y la función de enlazado o cálculo final.

La implementación es la siguiente:

const fork = function(fnJoin, fn1, fn2) {
    return function(val) {
        return fnJoin(fn1(val), fn2(val));
    }
}

Nos es muy útil para casos matemáticos. Por ejemplo, creamos una función que obtenga la media de un array. Podríamos hacerlo así:

const getAvarage = fork(R.divide, R.sum, R.length);
getAvarage([5, 7, 10]);

Conclusión

Los combinadores nos va a ser muy útiles para poder enlazar nuestra funcionalidad y poder controlar el flujo de nuestra aplicación. Con todo lo que hemos aprendido por el momento, ya podríamos empezar a desarrollar aplicaciones de una nueva forma.

Los siguientes módulos los vamos a dedicar a aprender sobre diferentes patrones de diseño que se suelen dar en programación funcional y que nos ayudarán a llegar a soluciones útiles y elegantes sobre problemas a los que se enfrenta el estilo funcional en su día a día.

Nos leemos 🙂

Anteriores posts de Programación Funcional en JavaScript:

Introducción

  1. La Programación Funcional en JavaScript
  2. Programación Funcional en JavaScript: Los Objetos
  3. Programación Funcional en JavaScript: Las funciones

El control de flujo funcional

  1. Programación Funcional en JavaScript: Los métodos funcionales
  2. Programación Funcional en JavaScript: La recursividad

La modularidad funcional

  1. Programación Funcional en JavaScript: La aridad y las tuplas
  2. Programación Funcional en JavaScript: La currificación
  3. Programación Funcional en JavaScript: La composición
  4. Programación Funcional en JavaScript: Los combinadores

Imagen de portada | Fox

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s