Reimplementando ramda desde 0
Nueva serie para implementar las utilidades de Ramda en Javascript vanilla
Esta serie que empiezo hoy va a ser mi comodín. Para que en esos días en los que aún he terminado de investigar sobre artículos más profundos , poder seguir generando contenido.
Así y como indica el título del post a lo largo de los artículos de esta colección voy a ir re-implementando en Javascript las utilidades de Ramda. Primero porque es divertido y segundo porque nos ayudaran a aplicar muchos de los conceptos que ya he ido comentando en otros artículos.
Para empezar y como es tradición cuando se aprenden lenguajes funcionales, voy a empezar por las utilidades de listas e iterables. El modus operandi será el siguiente: Miramos la documentación, buscaremos la utilidad en cuestión, entenderemos su descripción, su anotación y su scope, y finalmente la implementaremos.
Voy a usar repl.it para probar los ejemplos. Al final del artículo encontrarás el link.
Head
Vamos a empezar por la famosa utilidad head, que dado un iterable nos devuelve el primer elemento de este. Si vamos a la documentación vemos:
Funciona con Arrays
y con String
pero nosotros vamos a hacer que funcione con cualquier cosa que implemente el protocolo iterable. Además vamos a devolver un tipo mucho más seguro que undefined
en los casos de error: un Maybe:
El usar iterables en lugar de tipos más cerrados como String
o Array
nos habilita el poder usar head con cualquier tipo que implemente el protocolo iterable. Por ejemplo, una lista infinita:
const infinityList =
[Symbol.iterator]() {
let i = 1;
return {
next() {
return { value: i++, done: false }
}
}
}
}
head(infinityList) // Maybe(1)
Recuerda que los tipos por defecto que implementan el protocolo iterable son:
Array
,String
,Map
,Set
yTypedArray
.
Además el Maybe
nos protege de los tipos vacíos, como []
, ‘‘
o un iterable cuya propiedad done al llamar al primer next sea true
.
No nos protege en cambio de los tipos que no sean iterables. Ya que Javascript es un lenguaje de tipado débil me parece contraproducente controlar errores de tipos que se salgan fuera del dominio que marca la documentación de la función.
Si quieres más información sobre Maybe puedes ver este artículo y si te interesa profundizar en tipos de datos infinitos puedes ver este otro.
Last
El homólogo a head es last. En lugar de devolver el primer elemento de una lista, devuelve el último.
Aquí no tiene tanto sentido extrapolar el uso a un tipo iterable como hicimos con head, puesto que (evidentemente) no hay último elemento en una lista infinita. Por tanto, tenemos dos opciones:
Delegar la responsabilidad de no usar last con colecciones infinitas, poniendo algún aviso en la documentación.
Aplicar la técnica de Narrowing types. Es decir en lugar de usar iterables, usar tipos más especifícos como Array y String.
Yo voy a usar la segunda opción:
Aquí nos aprovechamos de los comportamientos por defecto que tiene Javascript para cubrir los casos extremos en los que se pase un tipo primitivo que no sea ni Array
ni String
, ya que:
1[0] // undefined
true[0] // undefined
Sin embargo, no nos libramos de los errores producidos por pasar un null
o un undefined
como parámetro a la función last. De nuevo, me parece overkill hacer esas comprobaciones aparte de las que vienen de gratis en la implementación nativa.
Rest
Después de head, la segunda utilidad que se enseñaba cuando aprendías algún lenguaje funcional (como Lisp) era rest, ya que con estas dos se habilitaba el uso de la recursividad sobre listas.
Car (/kɑːr/) y Cdr (/ˈkʊdər/)
Me parece interesante recordar esto aquí porque puede que no todos lo sepan. Estas eran las dos operaciones básicas para trabajar con listas después del cons en Lisp o Scheme.
El car de una lista hacía referencia a su primer elemento y el cdr al resto de elementos sin el primero. Como ya he mencionado, esto es lo que se utilizaba para hacer recursividad:
Cogíamos el car de una lista.
Hacíamos operaciones con el.
Y llamabamos de nuevo a la función pasándole el cdr de la lista (el resto de elementos sin el primero).
Cuando el car de una sublista era nulo, parábamos la recursividad.
En motores donde la recursividad no está optimizada usando Tail-recursion como es el caso de V8, no tiene mucho sentido usarla ya que implica aumentar el tamaño del stack y para listas muy grandes es contraproducente y puede llevar a un stack-overflow.
La recursividad ha sido históricamente usada porque para determinados problemas es más sencillo pensar en términos de sub-problemas que utilizar iteración. Por ejemplo, es mucho más intuitivo crear una función que recorra un árbol con recursividad que usando bucles, ya que la hoja de una raíz es a su vez otro árbol.
Bien, veamos la documentación de Ramda para la utilidad rest, aunque ellos la han llamado tail:
De nuevo solo funciona para los tipos Array
y String
y me parece razonable, ya que de nuevo iteradores infinitos no tienen cabida aquí.
Al final lo que está haciendo un rest/tail es hacer un slice sobre un Array
. Por tanto vamos a reprogramar slice
primero:
Me encuentro hoy un poco perezoso, por lo que en lugar de programar un slice, he reusado el que ya me da nativamente Array
y String
. Simplemente he arreglado un poco el API haciendo que se pueda aplicar parcialmente pasando primero el rango y después la colección.
Así ahora para programar la utilidad rest solo tenemos que:
Elegante y nos aprovechamos del control de casos extremos que ya nos aporta el método nativo.
Básicamente un rest es hacer un slice a partir del primer elemento hasta el final del Array
. Uso Infinity para poder mantener todo en un estilo point-free y porque sé que el slice nativo controla que como máximo se trocee la colección hasta length - 1
.
Reduce
También conocido como fold y es la puerta a poder implementar después las archiconocidas utilidades de map y filter.
Vamos a seguir el mismo enfoque que con head y lo vamos a extrapolar a iterables al contrario que el método reduce de Ramda que solo vale para Arrays
:
Un for .. of
es una estructura que nos permite iterar iterables. De esta forma podemos usar nuestro reduce de igual forma que usaríamos el nativo:
reduce((a, b) => a + b, 0)([1,2,3]) // 6
Y además podemos usarlos con strings y con cualquier iterable:
const ten = {
[Symbol.iterator]() {
let i = 1;
return {
next() {
return {
value: i++,
done: (i > 11),
}
}
}
}
}
console.log(reduce((a, b) => a + b, 0)(ten)) // 55
Map
Ahora con reduce podemos implementar map en una línea:
La peculiaridad aquí es que tenemos que saber el tipo que devuelve map por lo que en este caso debemos aplicar de nuevo la técnica de Narrowing Types y reducir el iterable del reduce a un Array
. Si quisiésemos un map para String
tendriamos crear uno específico para String
.
Otra opción quizá más elegante es reusar la implementación nativa de map ya que esta funciona tanto para Array como String. El problema es que al estar metido dentro de Array.prototype
el API nativo solo nos permite usarlo en Arrays. Pero podemos sacarlo:
const map = f => col => Array.prototype.map.call(col, f);
El problema es que el tipo de retorno sigue siendo un Array aunque le pasemos un String por lo que es una solución a medias.
Lo ideal es reimplementar map para cada tipo como en este artículo implementamos un map solo para iterables.
Puedes consultar los ejemplos de hoy aquí.
Nada más por hoy :).
Si te gustó mi artículo en El rincón del front déjame un like, si te encantó compártelo y si te flipó suscríbete para no perderte el siguiente! :)
[portada]: Foto de Ankush Minda