Reimplementando ramda desde 0: Parte 2
Implementando generadores de listas y funciones decoradoras
En el artículo de hoy vamos a continuar implementando las utilidades de ramda e iremos comentando curiosidades y posibles casos de uso. La idea final de esta serie es que vayas viendo todo el poder que tienen las funciones puras y que solo con ellas y sin usar ninguna estructura de lenguaje poder construir cualquier cosa.
Será cortito pero potente.
En el artículo anterior estuvimos implementando las utilidades más básicas para operar sobre listas: head, last, rest, reduce y map. Hoy las complementaremos con filter y unfold. Además, introduciremos el concepto de decoradora y crearemos algunas funciones aprovechando esta nueva idea.
Filtrar
Aparte de reducir una lista y aplicar transformaciones sobre sus elementos con map, la tercera habilidad que necesitamos es la de dada una lista poder sacar los elementos que no cumplan una determinada condición, generando eso si una nueva lista sin esos elementos.
Recuerda que la clave para que las funciones sean puras es la inmutabilidad. Por eso todas las funciones que manipulen de alguna forma los tipos de datos, tienen por el camino que ir creando estructuras nuevas.
La documentación de ramda sobre filter es la siguiente:
Un filter toma un Filterable, una función predicado (que toma un valor a y devuelve un Boolean), un functor de valores de tipo a
y devuelve otro functor de valores de tipo a
.
Nosotros no vamos a trabajar sobre cualquier functor, sino solo sobre listas.
De igual forma que en el artículo anterior usamos un reduce para implementar un map, aquí vamos a hacer lo mismo con filter:
function filter(pred) {
return function (arr) {
return reduce((acc, x) => {
if (pred(x)) {
acc.push(x);
}
return acc;
}, [])(arr);
};
}
Hacemos que la función sea curried tomando solo un parámetro a la vez y devolviendo una nueva función que tome el siguiente argumento.
Como veis estamos reduciendo una lista a otra lista donde solo nos quedamos con los elementos que cumplen nuestro predicado. Así por ejemplo un caso de uso podría ser el eliminar un conjunto de elementos de una lista:
filter(x => x % 2 === 0)([1,2,3,4,5]) // [2,4]
Aquí nos estamos quedando solo con los elementos pares de la lista que le proporcionamos.
Creando listas
No podemos filtrar o reducir listas si no tenemos listas. La siguiente utilidad que vamos a implementar se denomina unfold y nos permite crear e inicializar listas usando una semilla. El concepto de generación aquí se basa en las funciones iteradoras, o lo que es lo mismo una función que está pensada primero para ejecutarse en un proceso iterativo y segundo que invocamos para obtener información de la iteración actual y de la siguiente.
Así, dicha función recibe una semilla y devuelve un truthy o falsy. Donde el falsy es cualquier expresión que se resuelva a false y truthy ha de ser una tupla que devuelva como primer elemento el valor actual y como segundo la siguiente semilla.
Parece un trabalenguas porque lo es, pero como siempre todo es más claro con un ejemplo de función iteradora:
const f = (n) => (n > 50 ? false : [-n, n + 10]);
Nuestra función toma un valor y ante una condición devuelve o ‘false’ — en caso de no cumplirse — o la tupla que hemos descrito antes. Está idea de valores falsy y truthy es la que nos va a permitir continuar o salir del proceso iterativo. Así nuestro n > 50 sería la condición de parada de una supuesta iteración.
Estamos desacoplando con esto el devenir de una iteración sin que la función f
sepa nada de esa iteración en si.
Vamos a implementar ahora la función unfold que usando funciones iteradoras, genere un nuevo array.
La documentación de ramda a este respecto dice:
Y una implementación podría ser:
function unfold(iteratorFn, seed) {
const res = [];
let iteratorRes = iteratorFn(seed);
while (iteratorRes && iteratorRes.length) {
res.push(iteratorRes[0]);
iteratorRes = iteratorFn(iteratorRes[1]);
}
return res;
}
Y ahora podemos usarla para crear nuevas listas con la función que hemos definido antes:
unfold(f, 10); // [-10, -20, -30, -40, -50]
En resumen, en lugar de crear un proceso iterativo que defina sus propias condiciones de parada y continuación, estamos delegando eso en una función externa, desacoplando por tanto los mecanismos de parada del proceso iterativo. Así podemos tener funciones que generen arrays listas para pasar a la acción con unfold.
Decoradoras
No, no me estoy refiriendo a amables personas que van a darle un nuevo y revolucionario aspecto a nuestro salón para que sea más ecléctico, si no a a funciones que modifican el comportamiento de otras o agregan funcionalidad sin modificar la original.
Hay veces en las que la generación de funciones nuevas se produce por un tipo de composición diferente, en la que funciones ‘decoran’ a otras con funcionalidad.
Así por ejemplo tenemos a la función thunkify que dada una función la convierte en lazy retrasando su ejecución. Este concepto quizá os suene de librerías como Redux donde la creación de actions a partir de action creators pueden ser retrasados para poder proporcionar en un punto más avanzado del programa una función dispatch.
Es una técnica que se complementa muy bien con el patrón de inyección de dependencias.
Por tanto, para definir una función que cree thunks, vamos a crear una función decoradora que dada una función, devuelva una nueva función que tome los argumentos que requiera la función original y que esta a su vez devuelva otra función que es la que ejecute la función original. Es decir, queremos poder hacer esto:
function actionCreator = dispatch => {
dispatch('test');
}
const actionCreatorLazy = thunkify(actionCreator);
const actionCreator = actionCreatorLazy(myDispatch);
actionCreator();
Lo importante de todo esto es que la función original permanece inalterada. Nosotros simplemente obtenemos una función nueva decorada, en este caso el ‘decorado’ es retrasar su ejecución.
function thunkify(fn) {
return function (...params) {
return function invoke() {
return fn(...params);
};
};
}
Nada raro. Hemos implementado lo que habíamos dicho que íbamos a hacer. Ahora podemos decorar funciones para retrasar su ejecución, volviéndolas así lazy.
Otro decorador que también puede resultar aparente en según qué casos son los que reducen el arity de una función. Tenemos por tanto a la función unary que decora una función de N parámetros — o lo que es lo mismo, de arity N — y la convierte en una función que solo toma un argumento. La implementación también es sencilla y al igual que como pasaba con thunkify, recibe una función y devuelve una nueva:
function unary(fn) {
return function (a) {
return fn(a);
}
}
Y por ejemplo:
const tuple = (a, b) => [a, b];
const justOne = unary(tuple);
justOne(1, 2); // [1, undefined]
Finalmente, vamos a ver el decorador flip, que dada una función de dos argumentos, devuelve una función que toma esos mismos argumentos, pero en orden inverso:
function flip (fn) {
return function (a, b) {
return fn(b, a);
}
}
Esto es muy útil a la hora de componer funciones que inicialmente no sea composible por tener que proporcionar primero determinados argumentos. Además, es posible crear derivaciones de funciones cuando cambiamos el orden de sus parámetros. Por ejemplo, si definimos un map así:
const map = (list, fn) => list.map(fn);
Esto es lo que llamamos un mapper, ya que, si aplicamos parcialmente la función, primero le proporcionamos una lista y nos devuelve una función que mapea la función que le demos sobre una lista.
En cambio, si tenemos esto:
cont mapWith = (fn, list) => list.map(fn);
Aplicando parcialmente primero le daríamos una función y entonces nos devolvería una nueva función que tomaría una lista con la que mapearse. Por eso a esta solución la conocemos como ‘mapWith’ y la única diferencia aparente con ‘mapper’ es el orden de sus argumentos.
Si todo esto de curried o aplicación parcial te suena a chino, tienes que echarle un vistazo a esto:
Con una utilidad flip podríamos derivar un mapper a un mapWith y viceversa.
Y es más, podemos hacer que nuestra función flip invierta el orden de los argumentos a la vez transforme a la función en curried:
function flipAndCurry (fn) {
return function (a) {
return function (b) {
fn(b,a);
}
}
}
Así:
const map = (list, fn) => list.map(fn);
const mapWith = flipAndCurry(map);
Solo con funciones creamos funciones nuevas decorando las ya existentes. Máxima reutilización.
Conclusión
Como puedes observar las funciones puras son muy interesantes para no solo componer si no crear variaciones pequeñas sobre funciones ya existentes. La idea de ir implementado las utilidades de ramda no es otra que la que vayas viendo como con funciones podemos resolver cualquier problema.
Dicho esto, es importante decir que las utilidades de ramda son mucho mejores en la práctica que las implementaciones que hoy hemos dado aquí. Cubren por lo general más tipos de datos, están probadas y son combinables unas con otras.
Nada más por hoy :).