Introdução à Programação Funcional: Paradigmas JavaScript

Publicados: 2022-03-11

A programação funcional é um paradigma de construção de programas de computador usando expressões e funções sem mutação de estado e dados.

Ao respeitar essas restrições, a programação funcional visa escrever um código mais claro de entender e mais resistente a bugs. Isso é conseguido evitando o uso de instruções de controle de fluxo ( for , while , break , continue , goto ) que tornam o código mais difícil de seguir. Além disso, a programação funcional exige que escrevamos funções puras e determinísticas que são menos propensas a apresentar bugs.

Neste artigo, falaremos sobre como fazer programação funcional usando JavaScript. Também exploraremos vários métodos e recursos JavaScript que tornam isso possível. No final, exploraremos diferentes conceitos associados à programação funcional e veremos por que eles são tão poderosos.

Antes de entrar na programação funcional, porém, é preciso entender a diferença entre funções puras e impuras.

Funções puras vs. impuras

Funções puras recebem alguma entrada e fornecem uma saída fixa. Além disso, eles não causam efeitos colaterais no mundo exterior.

 const add = (a, b) => a + b;

Aqui, add é uma função pura. Isso ocorre porque, para um valor fixo de a e b , a saída será sempre a mesma.

 const SECRET = 42; const getId = (a) => SECRET * a;

getId não é uma função pura. A razão é que ele usa a variável global SECRET para calcular a saída. Se SECRET for alterado, a função getId retornará um valor diferente para a mesma entrada. Portanto, não é uma função pura.

 let id_count = 0; const getId = () => ++id_count;

Essa também é uma função impura, e também por algumas razões: (1) ela usa uma variável não local para calcular sua saída e (2) ela cria um efeito colateral no mundo exterior modificando uma variável nesse mundo.

getId é ilustração impura

Isso pode ser problemático se tivermos que depurar esse código.

Qual é o valor atual de id_count ? Quais outras funções estão modificando id_count ? Existem outras funções que dependem de id_count ?

Por esses motivos, usamos apenas funções puras em programação funcional.

Outro benefício das funções puras é que elas podem ser paralelizadas e memorizadas. Dê uma olhada nas duas funções anteriores. É impossível paralelizá-los ou memorizá-los. Isso ajuda na criação de código de alto desempenho.

Os Princípios da Programação Funcional

Até agora, aprendemos que a programação funcional depende de algumas regras. Eles são os seguintes.

  1. Não altere dados
  2. Use funções puras: saída fixa para entradas fixas e sem efeitos colaterais
  3. Usar expressões e declarações

Quando satisfazemos essas condições, podemos dizer que nosso código é funcional.

Programação Funcional em JavaScript

O JavaScript já possui algumas funções que possibilitam a programação funcional. Exemplo: String.prototype.slice, Array.protoype.filter, Array.prototype.join.

Por outro lado, Array.prototype.forEach, Array.prototype.push são funções impuras.

Pode-se argumentar que Array.prototype.forEach não é uma função impura por design, mas pense nisso - não é possível fazer nada com ela, exceto alterar dados não locais ou causar efeitos colaterais. Assim, não há problema em colocá-lo na categoria de funções impuras.

Além disso, o JavaScript tem uma declaração const, que é perfeita para programação funcional, pois não estaremos alterando nenhum dado.

Funções puras em JavaScript

Vejamos algumas das funções puras (métodos) fornecidas pelo JavaScript.

Filtro

Como o nome sugere, isso filtra a matriz.

 array.filter(condition);

A condição aqui é uma função que obtém cada item do array, e deve decidir se deve manter o item ou não e retornar o valor booleano verdadeiro para isso.

 const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]

Observe que filterEven é uma função pura. Se fosse impuro, teria tornado impura toda a chamada de filtro.

Mapa

map mapeia cada item do array para uma função e cria um novo array com base nos valores de retorno das chamadas de função.

 array.map(mapper)

mapper é uma função que recebe um item de um array como entrada e retorna a saída.

 const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]

Reduzir

reduce reduz a matriz a um único valor.

 array.reduce(reducer);

reducer é uma função que pega o valor acumulado e o próximo item no array e retorna o novo valor. É chamado assim para todos os valores na matriz, um após o outro.

 const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6 

reduzir ilustração de chamada

Concatenar

concat adiciona novos itens a um array existente para criar um novo array. É diferente de push() no sentido de que push() altera os dados, o que os torna impuros.

 [1, 2].concat([3, 4]) // [1, 2, 3, 4]

Você também pode fazer o mesmo usando o operador spread.

 [1, 2, ...[3, 4]]

Object.assign

Object.assign copia valores do objeto fornecido para um novo objeto. Como a programação funcional é baseada em dados imutáveis, nós a usamos para criar novos objetos baseados em objetos existentes.

 const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2

Com o advento do ES6, isso também pode ser feito usando o operador spread.

 const newObj = {...obj};

Criando sua própria função pura

Podemos criar nossa função pura também. Vamos fazer um para duplicar uma string n vezes.

 const duplicate = (str, n) => n < 1 ? '' : str + duplicate(str, n-1);

Esta função duplica uma string n vezes e retorna uma nova string.

 duplicate('hooray!', 3) // hooray!hooray!hooray!

Funções de ordem superior

Funções de ordem superior são funções que aceitam uma função como argumento e retornam uma função. Muitas vezes, eles são usados ​​para adicionar à funcionalidade de uma função.

 const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };

No exemplo acima, criamos uma função de ordem superior withLog que recebe uma função e retorna uma função que registra uma mensagem antes que a função encapsulada seja executada.

 const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7

withLog HOF também pode ser usado com outras funções e funciona sem conflitos ou sem escrever código extra. Esta é a beleza de um HOF.

 const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!

Pode-se também chamá-lo sem definir uma função de combinação.

 withLog(hype)('Sale'); // calling hype // Sale!!!

Escovando

Curry significa dividir uma função que recebe vários argumentos em um ou vários níveis de funções de ordem superior.

Vamos pegar a função add .

 const add = (a, b) => a + b;

Quando vamos curá-lo, nós o reescrevemos distribuindo argumentos em vários níveis como segue.

 const add = a => { return b => { return a + b; }; }; add(3)(4); // 7

O benefício do currying é a memorização. Agora podemos memorizar certos argumentos em uma chamada de função para que possam ser reutilizados posteriormente sem duplicação e re-computação.

 // assume getOffsetNumer() call is expensive const addOffset = add(getOffsetNumber()); addOffset(4); // 4 + getOffsetNumber() addOffset(6);

Isso é certamente melhor do que usar os dois argumentos em todos os lugares.

 // (X) DON"T DO THIS add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());

Também podemos reformatar nossa função curry para parecer sucinta. Isso ocorre porque cada nível da chamada da função currying é uma instrução de retorno de linha única. Portanto, podemos usar funções de seta no ES6 para refatorá-lo da seguinte maneira.

 const add = a => b => a + b;

Composição

Em matemática, a composição é definida como a passagem da saída de uma função para a entrada de outra, de modo a criar uma saída combinada. O mesmo é possível na programação funcional, pois estamos usando funções puras.

Para mostrar um exemplo, vamos criar algumas funções.

A primeira função é range, que recebe um número inicial a e um número final b e cria uma matriz que consiste em números de a a b .

 const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];

Então temos uma função multiplicar que pega um array e multiplica todos os números nele.

 const multiply = arr => arr.reduce((p, a) => p * a);

Usaremos essas funções juntas para calcular o fatorial.

 const factorial = n => multiply(range(1, n)); factorial(5); // 120 factorial(6); // 720

A função acima para cálculo fatorial é semelhante a f(x) = g(h(x)) , demonstrando assim a propriedade de composição.

Palavras finais

Passamos por funções puras e impuras, programação funcional, os novos recursos JavaScript que ajudam com isso e alguns conceitos-chave em programação funcional.

Esperamos que esta peça desperte seu interesse em programação funcional e possivelmente o motive a experimentá-la em seu código. Temos certeza de que será uma experiência de aprendizado e um marco em sua jornada de desenvolvimento de software.

A programação funcional é um paradigma bem pesquisado e robusto de escrever programas de computador. Com a introdução do ES6, o JavaScript permite uma experiência de programação funcional muito melhor do que nunca.