Programação Funcional em JavaScript usando Ramda
const R = require('ramda') const _ = require('lodash')
Programação funcional é um tema que vem ganhando tração no mundo do JavaScript, principalmente por algumas características da linguagem que permitem que muita coisa seja feita nesse estilo e pelas vantagens que esse paradigma traz pro desenvolvimento web. Entretanto o JavaScript não é uma linguagem funcional e possui diversas limitações e para superá-las é necessário o uso de bibliotecas.
Lodash e Underscore trouxeram para o JavaScript uma maneira mais declarativa de transformar dados e compor novas funções. Uma das principais inovações dessas bibliotecas foi o uso de chains para transformar dados. Ainda assim, esse estilo de escrita difere bastante da forma usada por linguagens funcionais e é isso que o Ramda tenta resolver.
Através do Ramda é possível escrever um sequência de funções nessa forma:
const result = R.pipe( R.prop('items'), R.filter(a => a !== undefined), R.map(R.add(1)) ) result({ items: [1, 2, undefined, 3]}) // [2, 3, 4]
É possível perceber que o dado não é referenciado na declaração da função, não há nenhuma variável que referencia o dado e isso permite que o desenvolvedor abstraia um pouco a execução da função e foque mais na composição das funções que vão gerar o resultado esperado.
Uma coisa interessante é que o Lodash permite esse tipo de escrita, só que é necessário fazer dois ajustes: fazer o curry da função e trocar a ordem dos parâmetros, assim o dado vai ser passado por último: E.g: map(array, fn) vira map(fn, array).
const get = _.curryRight(_.get, 2) const filter = _.curryRight(_.filter) const map = _.curryRight(_.map) const add = _.curry(_.add) const result = _.flow([ get('items'), filter(a => a !== undefined), map(add(1)) ]) result({ items: [1, 2, undefined, 3]}) // [2, 3, 4]
Dessa forma todas as funções desse pipe vão receber um array no último parâmetro, e enquanto elas não tiverem todos seus parâmetros preenchidos, elas vão retornar outra função, fazendo com que esse pipe execute de forma lazy, só executando quando receber o dado passado no último parâmetro. Essa é a principal diferença entre Ramda e Lodash/Underscore, todas as funções têm curry e o dado é passado no último parâmetro.
Para entender mais sobre currying leia o artigo Programação Funcional Parte 2.
Até agora não foram mostradas muitas vantagens em relação ao Lodash além do jeito de escrever, que é algo bastante subjetivo. Mas existe algo que o Ramda permite fazer mais facilmente que é a inclusão de funções customizadas. Para fazer isso no Lodash seria necessário incluir essa função utilizando o _.mixin().
_.mixin({ stringToChar: n => String.fromCharCode(97 + n) }) const result = arr => _.chain(_.get(arr, 'items')) .filter(a => a !== undefined) .head() .stringToChar() .value() result({ items: [undefined, 1, 3]}) // b
Enquanto que com Ramda basta escrever uma função normal em JS.
const stringToChar = n => String.fromCharCode(97 + n) const notUndefined = a => a !== undefined const result = R.pipe( R.prop('items'), R.filter(notUndefined), R.head, stringToChar ) result({ items: [undefined, 1, 3]}) // b
Imutabilidade
As funções da biblioteca não modificam os dados recebidos nos parâmetros. Isso é importante pois imutabilidade é um dos princípios da programação funcional e garante maior segurança.
Para entender mais sobre imutabilidade leia o artigo Programação Funcional Parte 1.
Composabilidade
Composição de funções é uma das coisas mais importantes ao se trabalhar com programação funcional. Composição pode ser definida como:
const compose = (f,g) => x => f(g(x))
Onde f e g são funções e x é o valor que vai ser passado para essas funções.
O Ramda oferece uma implementação mais robusta da função compose(), podendo receber inúmeras funções de aridade um e retornando uma função que recebe uma estrutura de dados. Assim que o dado for passado a sequência de funções vão ser executadas da direita para a esquerda.
const composed = R.compose(R.filter(notUndefined), R.prop('friends'))
Os exemplos anteriores usavam uma função chamada pipe() que tem um comportamento parecido com o do compose() mas que executa da esquerda da direita sendo assim mais fácil de ler.
Lens
Lens é um objeto que possui um getter e um setter para uma subestrutura na qual a lens foi “focada”. Ela recebe duas funções: uma função que age como getter e outra como setter, e retorna um objeto do “tipo” Lens. É possível entender lens como uma lente que foca em uma parte do dado.
cost lensX = R.lens(R.prop('x'), R.assoc('x'))
A função prop recebe o nome de uma propriedade e um objeto e retorna o valor de uma propriedade naquele objeto; é o nosso getter.
Já a função assoc() recebe o nome de uma propriedade, um valor e um objeto e retorna um novo objeto com a propriedade tendo um novo valor; é o nosso setter.
Junto com o lens, o Ramda traz algumas funções que recebem o lens como parâmetro, elas são view(), set() e over().
View() permite tu ver aonde a lens (lente) está focando
const person = { name: 'Marcos' } const lensName = R.lens(R.prop('name'), R.assoc('name')) R.view(lensName, person) // 'Marcos'
Set() serve para mudar o valor de uma propriedade
const person = { name: 'Marcos' } const lensName = R.lens(R.prop('name'), R.assoc('name')) R.set(lensName, 'Joao', person) // { name: 'Joao' }
Over() vai mudar o valor da propriedade passando ela por dentro de uma função
const person = { name: 'Marcos' } const lensName = R.lens(R.prop('name'), R.assoc('name')) R.over(lensName, x => x + ' Silva', person) // { name: 'Marcos Silva' }
Caso de Uso de Lens
Imagine que queremos filtrar posts de um usuário pelo número de likes. Usando somente composição não teríamos como fazer essa filtragem, pois a função filter() vai retornar um array e não todo o objeto que estamos trabalhando.
const user = { name: 'Marcos', posts: [ { title: 'Title 1', likes: 1 }, { title: 'Title 2', likes: 4 }, ] } const fn = R.pipe( R.prop('posts'), R.sort((a,b) => b.likes > a.likes) ) fn(user) // [{ title: 'Title 2', likes: 4 }, { title: 'Title 1', likes: 1 }]
Com lens isso é possível
const user = { name: 'Marcos', posts: [ { likes: 1, title: 'Title 1' }, { likes: 4, title: 'Title 2' }, ] } const lensPosts = R.lensProp('posts') const fn = R.over(lensPosts, R.sort((a,b) => b.likes > a.likes)) fn(user) // { // name: 'Marcos', // posts: [ // { likes: 4, title: 'Title 2' }, // { likes: 1, title: 'Title 1' } // ] // }
Lens também podem ser compostas, mas a ordem da composição é inversa, vai da esquerda pra direita
const user = { name: 'Marcos', posts: [ { likes: 1, title: 'Title 1' }, { likes: 4, title: 'Title 2' }, ] } const firstPostLens = R.compose(R.lensProp('posts'), R.lensIndex(0)) R.view(firstPostLens, user) // { likes: 1, title: 'Title 1' }
Conclusão
Espero que esse artigo tenha aumentado a curiosidade de vocês sobre programação funcional e como é possível começar a usar um pouco dela no mundo JavaScript através de bibliotecas como o Ramda ou outras que seguem a mesma linha como o Lodash/fp e o Sanctuary.
Deixe nos comentários suas dúvidas ou críticas e até a próxima.