Pressione enter para ver os resultados ou esc para cancelar.

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.