Front End

21 jul, 2017

Programação funcional em JavaScript usando Ramda

Publicidade

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 uma 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]
Array (3 items) 
0: 2
1: 3
2: 4
Array Prototype

É possível perceber que o dado não é referenciado na declaração da função e não há nenhuma variável que referencia o dado. 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]
Array (3 items)
0: 2
1: 3
2: 4
Array Prototype

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, 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
"b"

Enquanto que com Ramda, basta escrever uma função normal em JavaScript.

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 vai ser executada 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 para a 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'
"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' }
Object
name: "Joao"

Over() vai mudar o valor da propriedade passando-a 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' }
Object
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 }]
Array (2 items)
0: Object {likes: 4, title: "Title 2"}
1: Object {likes: 1, title: "Title 1"}
Array Prototype

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' }
//  ]
// }
Object
name: "Marcos"
posts: [, ]

Lens também podem ser compostas, mas a ordem da composição é inversa, vai da esquerda para 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' }
Object
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.

***

Este artigo foi publicado originalmente em: http://blog.taller.net.br/programacao-funcional-javascript-ramda/