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 oname
constante) - E finalmente o
makeFunction
retorna odisplayName
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-linerconst makeFunction = (name = 'TK') => () => console.log(name);
Agora podemos brincar com o nome:
const myFunction = makeFunction();myFunction(); // TKconst 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 pilhapop
: remova o primeiro item da pilhapeek
: obtém o primeiro item da pilhaisEmpty
: verifique se a pilha está vaziasize
: 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(); // truestack.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(); // 5stack.size(); // 5stack.isEmpty(); // falsestack.pop(); // [4, 3, 2, 1]stack.pop(); // [3, 2, 1]stack.pop(); // [2, 1]stack.pop(); // [1]stack.isEmpty(); // falsestack.peek(); // 1stack.pop(); // []stack.isEmpty(); // truestack.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.
Recursos