Programación funcional con Javascript (IV)

Programando con Ramda


En entregas anteriores, hemos comentado algunos de los beneficios de Ramda, más allá de algunas funciones. A todas las funciones se les ha aplicado curry y normalmente esperan el dato sobre el que aplicar la función como último parámetro favoreciendo la composición.
 

De nuevo map, filter y reduce

Como quizás ya habréis supuesto, Ramda posee map, filter, reduce, slice, concat… y otras muchas funciones para trabajar con listas. Veamos algunos de los ejemplos que hicimos antes, pero ahora, aplicando las funciones de Ramda.

const letters = ['a', 'b', 'c', 'd']
R.map(R.toUpper)(letters)
// ['A', 'B', 'C', 'D']

En este caso, la función que recibe map solo tendrá un único argumento. Veamos ahora filter y su opuesto, reject.

R.filter(n => n % 2 === 0)([1, 2, 3, 4, 5, 6, 7, 8, 9])
// [2, 4, 6, 8]
R.reject(n => n % 2 === 0)([1, 2, 3, 4, 5, 6, 7, 8, 9])
// [1, 3, 5, 7, 9]

En el primer caso filtramos todos aquellos números impares y con reject filtramos los pares.
 
Ahora reduce y otras alternativas.

R.reduce(R.add, 0)([1, 2, 3, 4, 5, 6, 7, 8, 9]) 
// 45

R.reduce(R.concat, '')(['a', 1, 'b', 2, 'c', 3]) 
// a1b2c3

R.invertObj(['one', 'two', 'three']) 
// { 'one': 0, 'two': 1, 'three': 2 }

R.pluck('name')([
  { name: 'Axl Rose', instrument: 'vocals' },
  { name: 'Slash', instrument: 'guitar' },
  { name: 'Izzy Stradlin', instrument: 'guitar' },
  { name: 'Steven Adler', instrument: 'drums' },
  { name: 'Duff McKagan', instrument: 'bass guitar' }
]) 
// [ 'Axl Rose', 'Slash', 'Izzy Stradlin', 'Steven Adler', 'Duff McKagan']

En los dos primeros casos usamos reduce, además usamos add o concat que son otras de las funciones disponibles.
 
En el tercero usamos invertObj, con el que de un Array obtenemos un Object al que le establece cada elemento del Array como key y a su vez cada index como valor para esa key. One, two y three pasan a ser las keys y 0, 1 y 2 sus valores.
 
Y ya en el último ejemplo, vemos pluck, que devuelve un Array con los valores para la key que se ha especificado como argumento. Similares a pluck tenemos bastantes funciones: prop, props, propOr, pick, path, pathOr
 
También tenemos sort, reverse, drop, find, flatten… todas puras, ninguna muta el array y además son deterministas. Además, forEach que ni muta el array ni devuelve nada, y por eso no lo incluyo con el resto.
const unsorted = [2, 1, 5, 4, 3]
R.sort(R.gt, unsorted)
// [1, 2, 3, 4, 5]
R.sort(R.lt, unsorted)
// [5, 4, 3, 2, 1]
R.reverse(unsorted)
// [3, 4, 5, 1, 2]

También en este ejemplo vemos gt (greatherThan) y lt (lowerThan) que podemos usar para comparar y, en este caso, con sort, ordenar, por ejemplo, un array.

Una vez repasadas (por encima) las funciones que ya habíamos visto previamente para Array, vamos a ver ahora algunas otras revisando las categorías en las que vienen divididas en Ramda.

Lists


Ya hemos visto algunas en el punto anterior. Son todas aquellas relacionadas con Arrays: búsquedas, transformaciones, filtros, conversiones a objetos, obtener subarrays… cualquier operación de las que ya hemos ido hablando. Por lo que no pararemos más tiempo en ello.

Objects


O lo que es lo mismo, operaciones con objetos: obtener valores de las propiedades, establecerlos, obtener arrays de keys o values, fusionar objetos… Pero hay un par de ellos que me gustaría destacar.

Lenses


Tenemos lens, lensProp, lensPath y lensIndex. Cualquiera de ellas nos servirá para hacer foco en una propiedad o índice y nos ayuda a obtener el valor de un objeto, establecer un valor o aplicar una función sobre esa propiedad o index.

const gunsAndRoses = {
  yearsActive: '1985-present',
  origin: 'Los Angeles',
  members: {
    /*...*/
  }
}
const originLens = R.lensProp('origin')

R.view(originLens, gunsAndRoses)
// 'Los Angeles'

R.set(originLens, 'Los Angeles, California', gunsAndRoses)
// {... 'origin': 'Los Angeles, California' ...}

R.over(originLens, R.replace('Los Angeles', 'LA'), gunsAndRoses)
// {... 'origin': 'LA' ...}

View nos devuelve la propiedad sobre la que apunta lensProp, set establece el valor que le pasamos como segundo argumento y over aplica la función sobre esa propiedad.
 
Puede que parezca poca cosa, pero es muy potente. Incluso se usa con setState de React y la verdad es que encaja perfectamente, por no decir que siempre podríamos crear con compose una batería de transformaciones sobre un objeto de manera sencilla.

Evolve


Similar a over, aplica una colección de transformaciones sobre un objeto. Unas normas a destacar:

  • Si la key no existe, no añadirá la propiedad
  • Si no es una función, no aplicará el valor

const theWho = {
  yearsActive: '1964-1982, 1989, 1996-present',
  origin: 'London',
  members: {
    singer: 'Roger Daltrey',
    guitarist: 'Pete Townshend',
    bassGuitarist: 'John Entwistle',
    drummer: 'Keith Moon'
  },
  followers: 787989,
  website: null,
  active: false
}

const bandUpdates = {
  bandName: 'The Who', // No existe la key, no aplica
  members: {
    drummer: R.always('Kenney Jones') // Es una función, aplica la transformación
  },
  followers: R.add(1500),
  website: R.always('theWho.com'),
  origin: 'London, UK', // No es una función, no aplica
  active: R.not
}

R.evolve(bandUpdates, theWho)
// {
//   yearsActive: '1964-1982, 1989, 1996-present',
//   origin: 'London',
//   members: {
//     singer: 'Roger Daltrey',
//     guitarist: 'Pete Townshend',
//     bassGuitarist: 'John Entwistle',
//     drummer: 'Kenney Jones'
//   },
//   followers: 789489,
//   website: 'theWho.com',
//   active: true
// }

Además, en este ejemplo de evolve vemos las siguientes funciones:

  • Always que al llamarse siempre devuelve el mismo valor
  • Add añade el argumento enviado
  • Not sería lo mismo que ! en JS

Function


En esta categoría se incluyen algunas de las que ya vimos como compose, pipe o curry, esta última también tiene su opuesto uncurry. Otras que podíamos destacar son:

Converge


Nos permite aplicar una lista de funciones sobre el mismo objeto, y el resultado de cada una es aplicado a la primera función que se pasa como argumento con los argumentos generados en cada una de las funciones.

const theWho = { 
  yearsActive: '1964-1982, 1989, 1996-present',
  origin: 'London',
  demonym: 'British',
  members: {
    singer: 'Roger Daltrey',
    guitarist: 'Pete Townshend',
    bassGuitarist: 'John Entwistle',
    drummer: 'Keith Moon'
  },
  followers: 787989,
  website: null,
  active: false
}

const acdc = {
  bandName: 'AC/DC'
  yearsActive: '1973-present',
  origin: 'Sydney',
  demonym: 'Australian',
  members: {
    singer: 'Brian Johnson',
    guitarist: 'Angus Young',
    bassGuitarist: 'Stevie Young',
    drummer: 'Chris Slade'
  }
}

const pickSinger = R.path(['members', 'singer'])
const pickDemonym = R.prop('demonym')
const pickBandName = R.propOr('', 'bandName')
const processBand = (singer, demonym, bandName) => `${singer} is the lead singer of the ${demonym} rock band ${bandName}`

R.converge(processBand, [pickSinger, pickDemonym, pickBandName])(acdc)
// Brian Johnson is the lead singer of the Australian rock band AC/DC

R.converge(processBand, [pickSinger, pickDemonym, pickBandName])(theWho)
// Roger Daltrey is the lead singer of the British rock band

Vemos como los resultados de las 3 funciones (pickSinger, pickDemonym, pickBandName) se pasa en el mismo orden a la función a ejecutar en converge (processBand) y en el mismo orden (Singer, demonym y bandName).

Flip, Unary y binary


En determinados momentos, nos encontraremos con situaciones en las que vamos a querer alterar el orden o número de argumentos de funciones de terceros (incluso de la propia librería Ramda). Estas funciones, y alguna otra, nos permiten ese tipo de operaciones.
 
Flip invierte el orden de los dos primeros argumentos. Unary y binary alteran el número de argumentos a uno y dos respectivamente.

const threeArgumentsFn = R.binary(R.flip((a, b, c) => `${a} ${b} ${c}`))
threeArgumentsFn('a', 'b', 'c')
// b a undefined

Usando __


El doble guión bajo (__) hace de placeholder para argumentos que aún no tenemos en ese momento o en situaciones en las que preferimos alterar el orden de los argumentos, por ejemplo, cuando si tenemos los datos que irían al final de la llamada y no tenemos alguno de los valores que suele ser constante.
 
Veamos un ejemplo que hará que quede más claro:

const pickMetallicaProp = R.path(R.__, metallica)
pickMetallicaProp(['members', 'singer']) 
// James Hetfield
pickMetallicaProp(['origin']) 
// Los Angeles

Ejemplo sencillo, pero espero que veáis el potencial que puede tener.

const insertInList = R.insert(R.__, R.__, [1, 2, 3, 4])
insertInList(1, 'Value at index 1') 
// [1, 'Value at index 1', 3, 4]
insertInList(4)('Value at end') 
// [1, 2, 3, 'Value at end']

Y mejor aún, como veis, podemos incluir más de uno y se le seguirá aplicando curry.

Tap


Ejecuta la función que se pasa como argumento al objeto que se incluye también como argumento y devuelve el objeto inicial.
 
Parece un buen sitio para llamar al impuro console.log y crear una función para hacer algo de debug entre llamadas de compose, entre otras aplicaciones mucho mejores que podrá tener.

const trace = R.tap(console.log)

const doSomeStuffWithBand = R.compose(
  trace, R.concat('Mr. '),
  trace, R.head,
  trace, R.reverse,
  trace, R.split(' '),
  trace, R.path(['members', 'drummer'])
) // Recordemos que compose va de derecha a izquierda

doSomeStuffWithBand(ledZeppelin) 
// 'Mr. Bonham'

Y aquí el log generado

John Bonham
['John','Bonham']
['Bonham','John']
Bonham
Mr. Bonham
John Bonham
['John','Bonham']
['Bonham','John']
Bonham
Mr. Bonham

Memoize


Comentábamos en otro de los posts que hay puntos en los que se podría hacer micro optimizaciones con memoize. Ramda nos provee de esa función.
 
Vamos a modificar un poco el propio ejemplo que nos da la documentación de Ramda y marcar los tiempos de ejecución.

let count = 0
const factorial = R.memoize(n => {
  count++
  return R.product(R.range(1, n + 1))
})

const factorialize = () => {
  console.time('factorialize')
  factorial(20000000)
  console.timeEnd('factorialize')
}

factorialize() // factorialize: 790.96484375ms
factorialize() // factorialize: 0.305908203125ms
factorialize() // factorialize: 0.044677734375ms
factorialize() // factorialize: 0.037841796875ms
factorialize() // factorialize: 0.06396484375ms
count // 1

Vemos que la primera ejecución se lleva unos milisegundos y el resto son casi inmediatas. Además vemos que count pese a llamar a la función 5 veces solo es 1.

Invoker


Ejecuta el método indicado en los argumentos sobre el objeto que se le pase.

fetch('https://api.spotify.com/v1/artists/5M52tdBnJaKSvOpJGz8mfZ')
  .then(R.invoker(0, 'json'))
  .then(R.pickAll(['name', 'popularity', 'type'])) 
// { 'name': 'Black Sabbath', 'popularity': 70, 'type': 'artist' }

En este caso, fetch devuelve un objeto (a través de una promesa) que tiene un método llamado json con cero argumentos, al obtenerlo devolvemos su resultado y obtenemos su nombre, popularidad y tipo.

T, F, Always e identity


Hay muchos más que ver, pero en determinados momentos necesitaremos una función que devuelva un valor en específico, ya vimos always que devuelve siempre lo que le hayamos pasado como argumento, también está T que devuelve siempre true o F que devuelve siempre false. O incluso algún caso en el que vamos a querer el propio objeto, en ese caso usaremos identity. Recordad esta última, ya que será importante para más adelante.

Logic


Esta categoría nos aporta funciones para aplicar lógica a la aplicación. Tenemos muchos como cond, when, ifElse, or, not.
 
Veamos un par de ejemplos sin para mucho más. Usaremos lo siguiente cómo datos para los ejemplos.

const blackSabbath = { name: 'Black Sabbath', category: 'Heavy Metal', tour: 'The End', ticketPrice: 140 }
const greenDay = { name: 'Green Day', category: 'Punk Rock', tour: '99 Revolutions Tour', ticketPrice: 80 }
const acdc = { name: 'AC/DC', category: 'Hard Rock', tour: 'Rock or Bust World Tour', ticketPrice: 200 }
const rollingStones = { name: 'The Rolling Stones', category: 'Rock', tour: '', ticketPrice: 130 }

const tours = [ blackSabbath, greenDay, acdc, rollingStones ]

Cond, es similar a un switch case, recibe un lista con arrays de 2 elementos, predicado y transformación. Si el predicado se cumple, se procesa la transformación y se devuelve el objeto sin pasar por el resto. Si no, se va pasando por los siguientes hasta encontrar alguno que satisfaga el predicado. En caso que no hubiese ninguno nos devolverá undefined, de ahí que en el ejemplo pongamos un caso por defecto.

const assistanceChooser = R.cond([
  [ R.propEq('category', 'Rock'), band => `I'll assit to the ${band.name} concert` ], // Es un concierto de categoría Rock?
  [ R.propSatisfies(R.lt(R.__, 100), 'ticketPrice'), R.always('As it's cheap, I'll assist to the concert') ], // Es un concierto de menos de 100?
  [ R.T, band => `I won't assist to ${band.name} concert`] // Default case
])

assistanceChooser(greenDay) 
// 'As it's cheap, I'll assist to the concert'
assistanceChooser(rollingStones) 
// 'I'll assit to the The Rolling Stones concert'
assistanceChooser(blackSabbath) 
// 'I won't assist to Black Sabbath concert'

When aplica la segunda función pasada como parámetro, si la primera devuelve true al ejecutarse contra el objeto. Además aquí usamos allPass, para indicar que queremos aplicar una lista de condiciones y que todas se tienen que cumplir.

const purchaseTicket = R.when(
  R.allPass([
    R.propEq('name', 'Black Sabbath'),
    R.propSatisfies(R.lt(R.__, 150), 'ticketPrice')
  ]),
  R.assoc('ticketPurchased', true)
)

purchaseTicket(rollingStones)
// No aplica ninguna transformación, no cumple ninguna de las condiciones de allPass
purchaseTicket(acdc)
// No aplica ninguna transformación, el precio es superior a 150
purchaseTicket(blackSabbath)
// Cumple todas las condiciones, añade ticketPurchased al objeto resultante
// {'category': 'Heavy Metal', 'name': 'Black Sabbath', 'ticketPrice': 140, 'ticketPurchased': true, 'tour': 'The End'}

Tenemos muchos otros: and, or, any, both, not, isEmpty

R.isEmpty(tours) // false, no esta vacio
R.not(R.isEmpty(tours)) // true, no esta vacio
R.not(R.isEmpty([])) // false, esta vacio

Type


Hay un grupo de funciones para hacer comprobaciones u obtener el tipo de dato. Como por ejemplo is.

R.is(Number, greenDay) // false
R.is(Object, acdc) // true

Relation


Para hacer comprobaciones, comprobaciones de rango, máximos, mínimos, sin un valor es mayor o menor que otro, si la propiedad o el path es igual a un valor. Ya hemos visto en los ejemplos algunos de ellos como gt, lt o propEq.

Math


Su nombre lo indica, operaciones matemáticas. Add, divide, multiply y algunos otros.

String


Operaciones con strings: toUpper, toLower, trim o replace.

CleanUp


Reconozco que visto con este tipo de ejemplos se rompe un poco la norma que comentábamos al principio de esta serie de posts, debe ser sencillo de leer y nuestros ejemplos tienen mucho paréntesis y corchete, tenemos R. por todas partes o arrow functions sueltas por el medio de expresiones. Así que vamos a poner un ejemplo ya más grande, pero también más ordenado.

import { map, compose, lensProp, over, propEq, T, cond, identity, match } from 'ramda'

const blackSabbath = { name: 'Black Sabbath', category: 'Heavy Metal', price: 140, purchased: 9376, total: 20000 }
const defLeppard = { name: 'Def Leppard', category: 'Rock', price: 125, purchased: 8423, total: 15000 }
const greenDay = { name: 'Green Day', category: 'Punk Rock', price: 80, purchased: 1425, total: 15000 }
const acdc = { name: 'AC/DC', category: 'Hard Rock', price: 200, purchased: 24765, total: 25000 }
const rollingStones = { name: 'The Rolling Stones', category: 'Rock', price: 130, purchased: 5671, total: 25000 }

const nextConcerts = [greenDay, defLeppard, blackSabbath, acdc, rollingStones]

//Lenses
const priceLens = lensProp('price')
const radioLens = lensProp('radio')
const purchasedLens = lensProp('purchased')

//Discounts
const calculateDiscount = perc => amt => amt - amt * (perc / 100)
const discount = d => over(priceLens, calculateDiscount(d))
const fivePercentDiscount = discount(5)
const tenPercentDiscount = discount(10)
const twentyPercentDiscount = discount(20)

//Other transformations not related with the price
const includeRadioAds = over(radioLens, T)
const partnershipRadioTickets = over(purchasedLens, add(300))

//Campaings predicates
const isAlmostFull = ({ total, purchased }) => total - purchased < 1000
const isGreenDay = propEq('name', 'Green Day')
const containsRock = compose(not, isEmpty, match(/(rock)/gi))
const hasRockCategory = propSatisfies(containsRock, 'category')

//Special campaings
const lastTicketsCampaing = compose(twentyPercentDiscount, includeRadioAds)
const radioRockCampaing = compose(fivePercentDiscount, includeRadioAds, partnershipRadioTickets)
const greenDayCampaing = tenPercentDiscount

//Map campaings with its transformations
const configureCampaings = cond([
  [isAlmostFull, lastTicketsCampaing],
  [isGreenDay, greenDayCampaing],
  [hasRockCategory, radioRockCampaing],
  [T, identity]
])

//Apply the campaings to all the concerts
const applyCampaings = map(configureCampaings)

//Run it!
applyCampaings(nextConcerts)

Inicialmente importamos (ES6 - import, ES6 - destructuring) las funciones para no tener que estar usando R. en cada una de ellas. Justo debajo, vemos los datos que usaremos en el ejemplo. Declaramos las lenses apuntando a las propiedades sobre las que vamos a aplicar transformaciones.
 
En el siguiente bloque creamos las funciones para aplicar descuentos, se ve que estamos usando el lens sobre la propiedad price. En el siguiente van otras transformaciones como indicar que se está anunciando en la radio o que se darán entradas a la radio para que las use como invitaciones.
 
A continuación, las condiciones que usaremos en las campañas, como: si apenas hay aforo, el grupo es Green Day o si pertenece a la categoría de Rock. Tras esto, las campañas a aplicar; si aplican varias usamos compose (de derecha a izquierda, aunque en esta ocasión no importa), sino directamente aplicamos el descuento. Ya casi al final configuramos el orden de las campañas y qué condiciones deben cumplir para aplicarlos. Y para terminar usamos map para aplicarlo a toda la lista, que incluso podríamos haber aplicado en la función anterior.
 
Son bastantes líneas, pero resulta sencillo ir comprendiendo qué pasa en cada sitio y además con unos nombres de función claros (seguro que se puede hacer mejor de lo que está) se puede entender rápido qué se está haciendo.
 
Como contra a todo esto, si cambias algo de la función deberías renombrar la función, por ejemplo si en twentyPercentDiscount aplicas un 30% deberías renombrarla o quizás mover la función discount con su porcentaje directamente a lastTicketsCampaing. Yo he preferido centrarme en que fuese legible.
 
A favor, legibilidad aparte, sin duda la reusabilidad, la dificultad para cometer errores al ser pedazos tan cortos de código además aplicando la funciones puras y demás conceptos es sencillo encontrar dónde se están cometiendo. Podríamos crear test unitarios por las fuciones pequeñas o ya por las que tienen un flujo mayor y no resultaría complejo.
 
Veamos qué resultado da el ejemplo:

[
  { category: 'Punk Rock', name: 'Green Day', price: 72, purchased: 1425, total: 15000 },
  { category: 'Rock' name: 'Def Leppard' price: 118.75 purchased: 8723 radio: true total: 15000 },
  { category: 'Heavy Metal' name: 'Black Sabbath' price: 140 purchased: 9376 total: 20000 },
  { category: 'Hard Rock' name: 'AC/DC' price: 160 purchased: 24765 radio: true total: 25000 },
  { category: 'Rock' name: 'The Rolling Stones' price: 123.5 purchased: 5971 radio: true total: 25000 }
]

Otras alternativas


Para finalizar me gustaría comentar que hay otras opciones que compiten con Ramda, tienen una visión similar pero con ciertas diferencias. Aquí os dejo los links para que podáis investigar:

Y otras librerías más destinadas a la inmutabilidad de los datos aunque con ciertas similitudes.
 

Resumen

Sin duda, en algunas de las funciones Ramda no inventan nada nuevo. Hace que parezca una librería con un conjunto de utilidades, pero sigue las filosofías que veíamos en posts anteriores y da ciertas ventajas como: curry en todas sus funciones, favorece la composición, sus funciones son puras, no mutan los datos sino que los transforman, es sencillo crear funciones con mucho potencial en una sola línea. Os dejo abajo los links a la librería para que podáis revisarla en detalle.


En la siguiente entrada veremos cómo trabajar con las estructuras de tipos específicos de FP, veremos qué los caracteriza y en qué nos beneficia.
 
Si te gusta la programación funcional, no te pierdas las entregas anteriores de esta serie Parte I, Parte II y Parte III.