#Princípios de Programação Funcional em Javascript #medio

Tempo de leitura: 10 minutos

Depois de muito tempo aprendendo e trabalhando com programação orientada a objetos, dei um passo atrás para pensar na complexidade do sistema e olhar uma outra perspectiva.

Fazendo algumas pesquisas, encontrei conceitos de programação funcional, como imutabilidade e funções puras. Esses conceitos permitem que você crie funções livres de efeitos colaterais, por isso é mais fácil manter sistemas – com alguns outros benefícios.

Neste post, falarei mais sobre programação funcional e alguns conceitos importantes, com muitos exemplos de código em JavaScript.

O que é programação funcional?

A programação funcional é um paradigma de programação – um estilo de construção da estrutura e dos elementos dos programas de computador – que trata a computação como a avaliação de funções matemáticas e evita a mudança de estado e dados mutáveis ​​- Wikipedia.

O primeiro conceito fundamental que aprendemos quando queremos entender a programação funcional são funções puras. Mas o que isso realmente significa? O que torna uma função pura?

Então, como sabemos se uma função é pura ou não? Aqui está uma definição muito simples de pureza:

  • Ela retorna o mesmo resultado se receber os mesmos argumentos (também é chamado de determinista)

  • Não causa efeitos colaterais observáveis

  • Ela retorna o mesmo resultado se receber os mesmos argumentos

Imagine que queremos implementar uma função que calcule a área de um círculo. Uma função impura receberia raio como o parâmetro e, em seguida, calcular raio  raio  PI:

let PI = 3.14;
const calculateArea = (radius) =\> radius * radius * PI;
calculateArea(10); // returns 314.0

Por que isso é uma função impura? Simplesmente porque ele usa um objeto global que não foi passado como um parâmetro para a função.

Agora, imagine alguns matemáticos argumentando que o valor do PI é realmente alterado e altere o valor do objeto global.

Nossa função impura agora resultará em 10  10  42 = 4200. Para o mesmo parâmetro (radius = 10), temos um resultado diferente.

Vamos consertar!

let PI = 3.14;
const calculateArea = (radius, pi) =\> radius * radius * pi;
calculateArea(10, PI); // returns 314.0

Agora vamos sempre passar o valor de PI como um parâmetro para a função. Então agora estamos apenas acessando parâmetros passados para a função. Nenhum objeto externo.

  • Para os parâmetros radius = 10 e PI = 3,14, sempre teremos o mesmo resultado: 314,0

  • Para os parâmetros radius = 10 e PI = 42, sempre teremos o mesmo resultado: 4200

Lendo arquivos

Se a nossa função lê arquivos externos, não é uma função pura – o conteúdo do arquivo pode mudar.

const charactersCounter = (text) =\> `Character count: ${text.length}`;

function analyzeFile(filename) {
  let fileContent = open(filename);
  return charactersCounter(fileContent);
}

Geração de números aleatórios

Qualquer função que depende de um gerador de números aleatórios não pode ser pura.

function yearEndEvaluation() {
  if (Math.random() \> 0.5) {
return "You get a raise!";
  } else {
return "Better luck next year!";
  }
}

Não causa efeitos colaterais observáveis

Exemplos de efeitos colaterais observáveis incluem modificar um objeto global ou um parâmetro passado por referência. Agora queremos implementar uma função para receber um valor inteiro e retornar o valor aumentado em 1.

let counter = 1;

function increaseCounter(value) {
  counter = value + 1;
}

increaseCounter(counter);
console.log(counter); // 2

Nós temos o valor do counter. Nossa função impura recebe esse valor e reatribui o contador com o valor aumentado em 1. Observação: a mutabilidade é desencorajada na programação funcional. Estamos modificando o objeto global. Mas como poderíamos torná-lo pure ? Apenas retorne o valor aumentado em 1.

let counter = 1;

const increaseCounter = (value) =\> value + 1;

increaseCounter(counter); // 2
console.log(counter); // 1

Veja que nossa função pura ** increaseCounter** retorna 2, mas o valor do contador ainda é o mesmo. A função retorna o valor incrementado sem alterar o valor da variável.

Se seguirmos essas duas regras simples, fica mais fácil entender nossos programas. Agora, cada função é isolada e incapaz de impactar outras partes do nosso sistema. Funções puras são estáveis, consistentes e previsíveis. Dados os mesmos parâmetros, as funções puras sempre retornarão o mesmo resultado. Não precisamos pensar em situações em que o mesmo parâmetro tenha resultados diferentes, porque isso nunca acontecerá.

Benefícios de funções puras

O código é definitivamente mais fácil de testar. Nós não precisamos zombar de nada. Assim, podemos testar funções puras com contextos diferentes:

  • Dado um parâmetro A → esperar que a função retorne o valor B
  • Dado um parâmetro C → esperar que a função retorne valor D

Um exemplo simples seria uma função para receber uma coleção de números e esperar que ela incrementasse cada elemento dessa coleção.

let list = [1, 2, 3, 4, 5]();

const incrementNumbers = (list) =\> list.map(number =\> number + 1);

Recebemos a matriz de números, usamos o mapa para incrementar cada número e retornamos uma nova lista de números incrementados.

incrementNumbers(list); // [2, 3, 4, 5, 6]()

Para a entrada 1, 2, 3, 4, 5, a saída esperada seria 2, 3, 4, 5, 6.

Imutabilidade

Quando os dados são imutáveis, seu estado não pode mudar depois de criado. Se você quiser mudar um objeto imutável, você não pode. Em vez disso, você cria um novo objeto com o novo valor.

Em JavaScript, geralmente usamos o loop for. Este próximo para a instrução tem algumas variáveis mutáveis.

var values = [1, 2, 3, 4, 5]();
var sumOfValues = 0;

for (var i = 0; i \< values.length; i++) {
  sumOfValues += values[i]();
}

sumOfValues // 15

Para cada iteração, estamos alterando o estado i e sumOfValue. Mas como lidamos com a mutabilidade na iteração? Recursão.

let list = [1, 2, 3, 4, 5]();
let accumulator = 0;

function sum(list, accumulator) {
  if (list.length == 0) {
return accumulator;
  }

  return sum(list.slice(1), accumulator + list[0]());
}

sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]()
accumulator; // 0

Então aqui temos a função sum que recebe um vetor de valores numéricos. A função chama a si mesma até obtermos a lista vazia (nosso caso base de recursão). Para cada “iteração”, adicionaremos o valor ao acumulador total.

Com a recursão, mantemos nossas variáveis imutáveis. A lista e as variáveis do acumulador não são alteradas. Mantém o mesmo valor.

Observação: Podemos usar reduzir para implementar essa função. Vamos cobrir isso no tópico de funções de ordem superior.

Também é muito comum construir o estado final de um objeto. Imagine que temos uma string e queremos transformar essa string em um slug de url. Em programação orientada a objetos em Ruby, criamos uma classe, digamos, UrlSlugify. E essa classe terá um método slugify para transformar a entrada de string em um slug de URL.

class UrlSlugify
  attr_reader :text

  def initialize(text)
@text = text
  end

  def slugify!
text.downcase!
text.strip!
text.gsub!(' ', '-')
  end
end

UrlSlugify.new(' I will be a url slug   ').slugify! # "i-will-be-a-url-slug"

Está implementado!

Aqui temos uma programação imperativa dizendo exatamente o que queremos fazer em cada processo de slugify – primeiro minúsculas, depois removemos espaços em branco inúteis e, finalmente, substituímos os espaços em branco restantes por hifens. Mas estamos mudando o estado de entrada nesse processo. Podemos lidar com essa mutação fazendo composição de funções ou encadeamento de funções. Em outras palavras, o resultado de uma função será usado como uma entrada para a próxima função, sem modificar a string de entrada original.

const string = " I will be a url slug   ";
const slugify = string =\>
  string
.toLowerCase()
.trim()
.split(" ")
.join("-");

slugify(string); // i-will-be-a-url-slug

Transparência referencial

Vamos implementar uma função square

const square = (n) =\> n * n;

Esta função pura sempre terá a mesma saída, dada a mesma entrada.

square(2); // 4
square(2); // 4
square(2); // 4
// ...

Passar 2 como um parâmetro da função square sempre retornará 4. Então, agora podemos substituir o square(2) por 4. Nossa função é referencialmente transparente.

Basicamente, se uma função produz consistentemente o mesmo resultado para a mesma entrada, ela é preferencialmente transparente.

funções puras + dados imutáveis = transparência referencial

Com esse conceito, uma coisa legal que podemos fazer é memorizar a função. Imagine que tenhamos essa função:

const sum = (a, b) =\> a + b;

E nós chamamos isso com esses parâmetros:

sum(3, sum(5, 8));

A sum(5, 8) é igual a 13. Essa função sempre resultará em 13. Então, podemos fazer isso:

sum(3, 13);

E essa expressão sempre resultará em 16. Podemos substituir toda a expressão por uma constante numérica e memorizá-la.

Funções como entidades de primeira classe

A ideia de funções como entidades de primeira classe é que as funções também são tratadas como valores e usadas como dados.

Funções como entidades de primeira classe podem:

  • Referem-se a ele de constantes e variáveis
  • Passá-lo como um parâmetro para outras funções
  • Devolvê-lo como resultado de outras funções

A ideia é tratar funções como valores e passar funções como dados. Desta forma, podemos combinar diferentes funções para criar novas funções com novo comportamento.

Imagine que tenhamos uma função que some dois valores e depois duplique o valor. Algo assim:

const doubleSum = (a, b) =\> (a + b) * 2;

Agora uma função que subtrai valores e retorna o dobro:

const doubleSubtraction = (a, b) =\> (a - b) * 2;

Essas funções têm lógica semelhante, mas a diferença são as funções dos operadores. Se pudermos tratar funções como valores e passá-las como argumentos, podemos construir uma função que receba a função de operador e a use dentro de nossa função.

const sum = (a, b) =\> a + b;
const subtraction = (a, b) =\> a - b;
const doubleOperator = (f, a, b) =\> f(a, b) * 2;

doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4

Agora temos um argumento f e o usamos para processar a e b. Nós passamos as funções de soma e subtração para compor com a função doubleOperator e criar um novo comportamento.

Funções de ordem superior

Quando falamos de funções de ordem superior, queremos dizer uma função que:

  • Toma uma ou mais funções como argumentos, ou
  • Retorna uma função como resultado

A função doubleOperator que implementamos acima é uma função de ordem mais alta porque ela usa uma função de operador como argumento e a usa. Você provavelmente já ouviu falar sobre filtro, mapa e reduzir. Vamos dar uma olhada nestes.

Filter

Dada uma coleção, queremos filtrar por um atributo. A função de filtro espera um valor true ou false para determinar se o elemento deve ou não ser incluído na coleção de resultados. Basicamente, se a expressão de retorno de chamada for true, a função de filtro incluirá o elemento na coleção de resultados. Caso contrário, não será. Um exemplo simples é quando temos uma coleção de inteiros e queremos apenas os números pares.

Um exemplo simples é quando temos uma coleção de inteiros e queremos apenas os números pares.

Abordagem imperativa

Uma maneira imperativa de fazer isso com JavaScript é:

  • criar um array vazio evenNumbers
  • iterar sobre a matriz de numbers
  • empurre os números pares para o array evenNumbers
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];

for (var i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 == 0) {
    evenNumbers.push(numbers[i]);
  }
}
console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

Também podemos usar a função de ordem superior do filter para receber a função par e retornar uma lista de números pares:

const even = n => n % 2 == 0;
const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]

Gostou do conteúdo? não deixe de seguir a uebile nas redes sociais, pois toda semana tem post novo aqui no blog com mais dicas para o seu impulso digital.