Brincando com closures, currying e abstrações legais


Este artigo foi publicado originalmente no Blog do TK.

Neste artigo, falaremos sobre fechamentos, funções ao curry e brincaremos com esses conceitos para criar abstrações interessantes. Quero mostrar a idéia por trás de cada conceito, mas também torná-lo muito prático com exemplos e refatorar o código para torná-lo mais divertido.

Encerramentos

Portanto, o fechamento é um tópico comum em JavaScript e vamos começar com ele. Como os documentos da web do MDN definem:

“Um fechamento é a combinação de uma função agrupada (anexada) com referências ao seu estado circundante (o ambiente lexical).”

Basicamente, toda vez que uma função é criada, também é criado um fechamento e dá acesso a todos os estados (variáveis, constantes, funções, etc.). O estado circundante é conhecido como lexical environment.

Vamos mostrar um exemplo simples:

function makeFunction() {
  const name = 'TK';
  function displayName() {
    console.log(name);
  }
  return displayName;
};

O que temos aqui?

  • Nossa principal função chamada makeFunction
  • Uma constante chamada name atribuído com uma string 'TK'
  • A definição do displayName função (que apenas registra o name constante)
  • E finalmente o makeFunction retorna o displayName função

Esta é apenas uma definição de uma função. Quando chamamos o makeFunction, ele criará tudo dentro dele: constante e função neste caso.

Como sabemos, quando o displayName é criada, o fechamento também é criado e conscientiza a função do ambiente, neste caso, a função name constante. É por isso que podemos console.log a name sem quebrar nada. A função conhece o ambiente lexical.

const myFunction = makeFunction();
myFunction(); // TK

Ótimo! Funciona como esperado! O retorno do makeFunction é uma função que armazenamos no myFunction constante, ligue mais tarde e exibe TK.

Também podemos fazê-lo funcionar como uma função de seta:

const makeFunction = () => {
  const name = 'TK';
  return () => console.log(name);
};

Mas e se quisermos passar o nome e exibi-lo? Um parâmetro!

const makeFunction = (name = 'TK') => {
  return () => console.log(name);
};

// Or a one-liner
const makeFunction = (name = 'TK') => () => console.log(name);

Agora podemos brincar com o nome:

const myFunction = makeFunction();
myFunction(); // TK

const myFunction = makeFunction('Dan');
myFunction(); // Dan

Nosso myFunction está ciente dos argumentos transmitidos: valor padrão ou dinâmico.
O fechamento torna a função criada não apenas ciente das constantes / variáveis, mas também de outras funções dentro da função.

Então, isso também funciona:

const makeFunction = (name = 'TK') => {
  const display = () => console.log(name);
  return () => display();
};

const myFunction = makeFunction();
myFunction(); // TK

A função retornada conhece o display função e é capaz de chamá-lo.

Uma técnica poderosa é usar fechamentos para criar funções e variáveis ​​”privadas”.

Meses atrás, eu estava aprendendo estruturas de dados (de novo!) E queria implementar cada uma. Mas eu estava sempre usando a abordagem orientada a objetos. Como entusiasta da programação funcional, eu queria construir todas as estruturas de dados seguindo os princípios do FP (funções puras, imutabilidade, transparência referencial, etc.).

A primeira estrutura de dados que eu estava aprendendo foi o Stack. É bem simples. A API principal é:

  • push: adicione um item ao primeiro local da pilha
  • pop: remova o primeiro item da pilha
  • peek: obtém o primeiro item da pilha
  • isEmpty: verifique se a pilha está vazia
  • size: obtém o número de itens que a pilha possui

Poderíamos criar claramente uma função simples para cada “método” e passar os dados da pilha para ele. Ele usa / transforma os dados e os retorna.

Mas também podemos criar dados de pilha privada e expor apenas os métodos da API. Vamos fazer isso!

const buildStack = () => {
  let items = [];

  const push = (item) => items = [item, ...items];
  const pop = () => items = items.slice(1);
  const peek = () => items[0];
  const isEmpty = () => !items.length;
  const size = () => items.length;

  return {
    push,
    pop,
    peek,
    isEmpty,
    size,
  };
};

Como criamos o items empilhar dados dentro de nossa buildStack função, é “privado”. Ele pode ser acessado apenas dentro da função. Nesse caso, apenas o push, pop, etc. podem tocar nos dados. E é isso que estamos procurando.

E como usamos? Como isso:

const stack = buildStack();

stack.isEmpty(); // true

stack.push(1); // [1]
stack.push(2); // [2, 1]
stack.push(3); // [3, 2, 1]
stack.push(4); // [4, 3, 2, 1]
stack.push(5); // [5, 4, 3, 2, 1]

stack.peek(); // 5
stack.size(); // 5
stack.isEmpty(); // false

stack.pop(); // [4, 3, 2, 1]
stack.pop(); // [3, 2, 1]
stack.pop(); // [2, 1]
stack.pop(); // [1]

stack.isEmpty(); // false
stack.peek(); // 1
stack.pop(); // []
stack.isEmpty(); // true
stack.size(); // 0

Portanto, quando a pilha é criada, todas as funções estão cientes da items dados. Mas fora da função, não podemos acessar esses dados. É privado. Nós apenas modificamos os dados usando a API incorporada à pilha.

Curry

“Currying é o processo de pegar uma função com vários argumentos e transformá-la em uma sequência de funções, cada uma com apenas um único argumento”. – Wikipedia

Imagine que você tenha uma função com vários argumentos: f(a, b, c). Usando currying, alcançamos uma função f(a) que retorna uma função g(b) o retorna uma função h(c).

Basicamente: f(a, b, c) -> f(a) => g(b) => h(c)

Vamos construir um exemplo simples: adicione dois números. Mas primeiro, sem currying!

const add = (x, y) => x + y;
add(1, 2); // 3

Ótimo! Super simples! Aqui temos uma função com dois argumentos. Para transformá-lo em uma função ao curry, precisamos de uma função que receba x e retorna uma função que recebe y e retorna a soma dos dois valores.

const add = (x) => {
  function addY(y) {
    return x + y;
  }

  return addY;
};

Podemos refatorar isso addY em uma função de seta anônima:

const add = (x) => {
  return (y) => {
    return x + y;
  }
};

Ou simplifique-o criando funções de seta de uma linha:

const add = (x) => (y) => x + y;

Essas três funções com caril diferentes têm o mesmo comportamento: construa uma sequência de funções com apenas um argumento.

Como usamos?

add(10)(20); // 30

No começo, pode parecer um pouco estranho, mas tem uma lógica por trás disso. add(10) retorna uma função. E chamamos essa função com o 20 valor.

É o mesmo que:

const addTen = add(10);
addTen(20); // 30

E isso é interessante. Podemos gerar funções especializadas chamando a primeira função. Imagine que queremos um increment função. Podemos gerá-lo a partir do nosso add função passando o 1 como o valor

const increment = add(1);
increment(9); // 10


Quando eu estava implementando o Cypress preguiçoso, uma biblioteca npm para registrar o comportamento do usuário em uma página de formulário e gerar o código de teste do Cypress. Quero criar uma função para gerar essa string input[data-testid="123"]. Então aqui temos o elemento (input), o atributo (data-testid) e o valor (123) A interpolação dessa string no JavaScript ficaria assim: ${element}[${attribute}="${value}"].

a primeira implementação em mente é receber esses três valores como parâmetros e retornar a sequência interpolada acima.

const buildSelector = (element, attribute, value) =>
  `${element}[${attribute}="${value}"]`;

buildSelector('input', 'data-testid', 123); // input[data-testid="123"]

E isso é ótimo. Consegui o que estava procurando. Mas, ao mesmo tempo, eu queria criar uma função mais idiomática. Algo que eu poderia escrever “obtenha um elemento X com o atributo Y e o valor Z“. E se quebrarmos essa frase em três etapas:

  • obter um elemento X“: get(x)
  • com atributo Y“: withAttribute(y)
  • e valor Z“: andValue(z)

Nós podemos transformar o buildSelector(x, y, z) para dentro get(x)withAttribute(y)andValue(z) usando o conceito de curry.

const get = (element) => {
  return {
    withAttribute: (attribute) => {
      return {
        andValue: (value) => `${element}[${attribute}="${value}"]`,
      }
    }
  };
};

Aqui usamos uma ideia diferente: retornar um objeto com função como valor-chave. Desta forma, podemos alcançar esta sintaxe: get(x).withAttribute(y).andValue(z).

E para cada objeto retornado, temos a próxima função e argumento.

Refatorando o tempo! Remova o return afirmações:

const get = (element) => ({
  withAttribute: (attribute) => ({
    andValue: (value) => `${element}[${attribute}="${value}"]`,
  }),
});

Eu acho que parece mais bonito. E usamos como:

const selector = get('input')
  .withAttribute('data-testid')
  .andValue(123);

selector; // input[data-testid="123"]

o andValue função sabe sobre o element e attribute valores porque está ciente do ambiente lexical como falamos sobre fechamentos antes.


Também podemos implementar funções usando “currying parcial”. Separe apenas o primeiro argumento do resto, por exemplo.

Fazendo desenvolvimento web por um longo tempo, eu geralmente usava o API da Web do ouvinte de eventos. É usado desta maneira:

const log = () => console.log('clicked');
button.addEventListener('click', log);

Eu queria criar uma abstração para criar ouvintes de eventos especializados e usá-los passando o elemento e o manipulador de retorno de chamada.

const buildEventListener = (event) => (element, handler) => element.addEventListener(event, handler);

Dessa forma, eu posso criar diferentes ouvintes de eventos especializados e usá-los como funções.

const onClick = buildEventListener('click');
onClick(button, log);

const onHover = buildEventListener('hover');
onHover(link, log);


Com todos esses conceitos, eu poderia criar uma consulta SQL usando a sintaxe JavaScript. Eu queria consultar SQL dados JSON como:

const json = {
  "users": [
    {
      "id": 1,
      "name": "TK",
      "age": 25,
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Kaio",
      "age": 11,
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Daniel",
      "age": 28,
      "email": "[email protected]"
    }
  ]
}

Então, criei um mecanismo simples para lidar com essa implementação:

const startEngine = (json) => (attributes) => ({ from: from(json, attributes) });

const buildAttributes = (node) => (acc, attribute) => ({ ...acc, [attribute]: node[attribute] });

const executeQuery = (attributes, attribute, value) => (resultList, node) =>
  node[attribute] === value
    ? [...resultList, attributes.reduce(buildAttributes(node), {})]
    : resultList;

const where = (json, attributes) => (attribute, value) =>
  json
    .reduce(executeQuery(attributes, attribute, value), []);

const from = (json, attributes) => (node) => ({ where: where(json[node], attributes) });

Com esta implementação, podemos iniciar o mecanismo com os dados JSON:

const select = startEngine(json);

E use-o como uma consulta SQL:

select(['id', 'name'])
  .from('users')
  .where('id', 1);

result; // [{ id: 1, name: 'TK' }]


Por hoje é isso. Poderíamos continuar mostrando exemplos diferentes de abstrações, mas agora eu deixo você brincar com esses conceitos.

Você pode encontrar este artigo e outros semelhantes em Blog do TK.

Minhas Twitter e Github.

Recursos