Desenvolvimento

6 jun, 2016

Programação funcional em JavaScript – desconstruindo o Pareto.js

Publicidade

*Artigo originalmente publicado no Medium pessoal do autor. Confira aqui.

Nos últimos tempos, só se fala em programação funcional, seus benefícios, funções puras, dados imutáveis, composição de funções etc. Caso você queira saber mais, recomendo este artigo do Matheus Lima.

Atualmente, temos diversas libs que auxiliam o JavaScript na missão de ser funcional, como Lodash, Underscore e Ramda. Neste artigo especificamente, vou falar sobre o Pareto.js, lib que criamos na Concrete Solutions que é simples como o Princípio de Pareto, e tem por objetivo ser leve e resolver 80% dos seus problemas com 20% de código.

Geralmente, procuro aprender algo que desmistifique a “mágica” por trás da implementação. Foi assim quando comecei a aprender Angular, e agora estou aplicando o mesmo à programação funcional. Por isso, neste texto vamos avaliar as implementações de Curry e Compose do Pareto.js.

Curry

Curry é a ação de pegar uma função que receba múltiplos argumentos e transformá-la em uma cadeia de funções, em que cada uma receba somente um parâmetro.

const curry = (fn, ...args) => {
    if (args.length === fn.length) {
        return fn(...args)
    }
    return curry.bind(this, fn, ...args)
}

Vamos agora ver o teste dessa função:

describe('curry', () => {
  it('returns the curried function', () => {
      const add = (a, b) => a + b

      expect(FunctionUtils.curry(add, 1, 2)).toBe(3)
      expect(FunctionUtils.curry(add)(1)(2)).toBe(3)
      expect(FunctionUtils.curry(add)(1, 2)).toBe(3)
      expect(FunctionUtils.curry(add, 1)(2)).toBe(3)
  })
})

Para começar a desmitificar a mágica, temos duas perguntas:

  • Como a nossa função Curry irá armazenar os parâmetros já passados?
  • O que o Function.prototype.bind() tem a ver com isso?

Function.prototype.bind()

Comumente usamos .bind() para passarmos um contexto para executar uma função, mas nos esquecemos de algo importante, como dito na documentação do developer.mozilla.org:

Partial Functions: The next simplest use of bind() is to make a function with pre-specified initial arguments. These arguments (if any) follow the provided this value and are then inserted at the start of the arguments passed to the target function…

Resumindo:

Um dos usos de bind() é construir uma função com argumentos iniciais pré-especificados. Esses argumentos serão passados após o valor de This e serão inseridos no início dos argumentos passados para a função de destino.

Difícil de entender? Então vamos a mais um exemplo (em ES5 para que você possa abrir o devtools e já testar).

"use strict";

function myNumbers(x, y, z){
  console.log(x);
  console.log(y);
  console.log(z);
}

var foo = myNumbers.bind(this, 1);
foo(); 
// 1
// undefined
// undefined

var bar = foo.bind(this, 2);
bar();
// 1
// 2
// undefined

var baz = bar.bind(this, 3);
baz();
//1
//2
//3

Reparem que a função myNumbers espera três parâmetros. A cada vez que chamamos .bind(this, val), a função retornada pelo método .bind() automagicamente guarda o argumento passado.

E com isso chegamos à implementação do curry no pareto.js, que irá chamar curry.bind(this, fn, …args), empilhando os parâmetros no spread operator …args até que a quantidade de argumentos seja a mesma que a função espera (args.length === fn.length). Caso não tenha entendido o que é …args, dê uma lida em spread operator.

Compose

Como o próprio nome sugere, Compose significa construir funções mais complexas por meio de funções mais simples, compondo-as. Vamos à implementação no Pareto.js:

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))

Vamos ao teste dessa função:

describe('compose', () => {
    it('composes functions', () => {
        const toUpperCase = x => x.toUpperCase()
        const exclaim = x => `${x}!`
        const moreExclaim = x => `${x}!!`

        expect(FunctionUtils.compose(toUpperCase, exclaim)('test')).toBe('TEST!')
        expect(FunctionUtils.compose(toUpperCase, exclaim, moreExclaim)('test')).toBe('TEST!!!')
    })
})

E aí temos uma pergunta:

  • O que Array.prototype.reduce() está fazendo aí no meio ?

Array.prototype.reduce()

Em geral, pensamos no .reduce() como um acumulador, porém somente no sentido de soma de valores e não de composição. Sabemos que o .reduce() aplica uma função de callback sobre um acumulador, varrendo todos os elementos do array. Vamos começar a desconstrução do nosso compose:

  • Sabemos que ele recebe um array de funções como argumentos por meio do spread operator …args;
  • A função de callback do .reduce(), que será executada sobre cada item do nosso array, pode receber até 4 parâmetros, sendo eles: previousValue, currentValue, index e array. Porém, aqui só iremos utilizar os dois primeiros (previousValue e currentValue). Lembrando que, na primeira chamada à nossa função de callback, previousValue será o valor do primeiro elemento do array e currentValue será o valor do elemento seguinte;
  • A nossa função de callback irá compor a função passada em previousValue com a que está em currentValue, adicionando na declaração da função que ela poderá receber N argumentos (…args). Resultando em previousValue(currentValue(…args)).

De acordo com os nossos testes, vamos observar os passos de execução em uma tabela:

tabela

Com isso, temos o resultado da função mais interna (moreExclaim) alimentando as funções mais externas (exclaim e depois to UpperCase).

E é isso! Espero que tenha ajudado a entender a relação de curry e compose com .bind() e .reduce(). Feedbacks são mais do que bem-vindos e incentivados, aproveite os campos abaixo. Até a próxima!