Lentes

Getters e Setters Composable para Programação Funcional

Eric Elliott Blocked Unblock Seguir Seguindo 23 de dezembro de 2018 Fumaça Art Cubes to Smoke – Mattys Flicks – (CC BY 2.0)

Observação: isso faz parte do livro “Compor software”, que começou bem aqui como uma série de posts no blog. Abrange a programação funcional e técnicas de software de composição em JavaScript (ES6 +) a partir do zero.
<Anterior | << Começar de novo na Parte 1

Uma lente é um par composto de funções de getter e setter puras que se concentram em um campo particular dentro de um objeto, e obedecem a um conjunto de axiomas conhecidos como leis da lente. Pense no objeto como o todo e o campo como a peça . O getter pega um todo e retorna a parte do objeto em que a lente está focada.

 // view = whole => part 

O setter pega um inteiro e um valor para definir a peça e retorna um novo inteiro com a parte atualizada. Ao contrário de uma função que simplesmente define um valor no campo de membro de um objeto, os Lens setters são funções puras:

 // set = whole => part => todo 

Nota: Neste texto, vamos usar algumas lentes ingênuas nos exemplos de código apenas para dar uma olhada no conceito geral. Para o código de produção, você deve procurar uma biblioteca bem testada como Ramda. A API difere entre diferentes bibliotecas de lentes e é possível expressar lentes de formas mais compostas e elegantes do que as apresentadas aqui.

Imagine que você tem uma matriz tupla representando um ponto x , y , e z coordenadas:

 [x, y, z] 

Para obter ou definir cada campo individualmente, você pode criar três lentes. Um para cada eixo. Você pode criar manualmente getters que se concentram em cada campo:

 const getX = ([x]) => x; 
const getY = ([x, y]) => y;
const getZ = ([x, y, z]) => z;
 console.log ( 
getZ ([10, 10, 100]) // 100
);

Da mesma forma, os setters correspondentes podem ter esta aparência:

 const setY = ([x, _, z]) => y => ([x, y, z]); 
 console.log ( 
setY ([10, 10, 10]) (999) // [10, 999, 10]
);

Por que lentes?

Dependências de forma de estado são uma fonte comum de acoplamento em software. Muitos componentes podem depender da forma de algum estado compartilhado, portanto, se você precisar alterar posteriormente a forma desse estado, será necessário alterar a lógica em vários locais.

As lentes permitem abstrair a forma do estado por trás de getters e setters. Em vez de colocar no lixo sua base de código com código que mergulha profundamente na forma de um objeto específico, importe uma lente. Se mais tarde você precisar alterar a forma do estado, poderá fazê-lo na lente, e nenhum dos códigos que dependem da lente precisará ser alterado.

Isso segue o princípio de que uma pequena alteração nos requisitos deve exigir apenas uma pequena alteração no sistema.

fundo

Em 1985, “Estrutura e Interpretação de Programas de Computador” descreveu os pares getter e setter (chamados put e get no texto) como uma forma de isolar a forma de um objeto do código que usa o objeto. O texto mostra como criar seletores genéricos que acessam partes de um número complexo independente de como o número é representado. Esse isolamento é útil porque quebra as dependências de formas do estado. Esses pares getter / setter eram um pouco como consultas referenciadas que existiam em bancos de dados relacionais há décadas.

As lentes levaram o conceito ainda mais, tornando os pares getter / setter mais genéricos e compostos. Eles foram popularizados depois que Edward Kmett lançou a biblioteca Lens para Haskell. Ele foi influenciado por Jeremy Gibbons e Bruno C. d. S. Oliveira, que demonstrou que as travessias expressam o padrão do iterador, os "assessores" de Luke Palmer, Twan van Laarhoven e Russell O'Connor.

Nota: Um erro fácil é equacionar a noção moderna de uma lente funcional com Anamorfismos, baseada em Erik Meijer, Maarten Fokkinga e “Programação Funcional com Bananas, Lentes, Envelopes e Arame Farpado” de Ross Paterson em 1991. “O termo "lente", no sentido de referência funcional, refere-se ao fato de olhar para parte de um todo. O termo 'lente' em um sentido de esquema de recursão refere-se ao fato de que [( e )] sintaticamente se parecem com lentes côncavas. tl; dr Eles não têm nada a ver um com o outro "~. Edward Kmett em Stack Overflow

Leis de lentes

As leis da lente são axiomas algébricos que garantem que a lente é bem comportada.

  1. view(lens, set(lens, a, store)) ? a – Se você definir um valor na loja e visualizar imediatamente o valor através da lente, você obtém o valor que foi definido.
  2. set(lens, b, set(lens, a, store)) ? set(lens, b, store) – Se definir um valor de objectiva para a e depois definir imediatamente o valor da objectiva para b , é o mesmo que se você d apenas defina o valor para b .
  3. set(lens, view(lens, store), store) ? store – Se você obtiver o valor da lente da loja e, em seguida, definir imediatamente esse valor de volta na loja, o valor permanecerá inalterado.

Antes de nos aprofundarmos nos exemplos de código, lembre-se de que, se você estiver usando lentes em produção, provavelmente deve estar usando uma biblioteca de lentes bem testada. O melhor que conheço no JavaScript é o Ramda. Nós vamos pular isso por enquanto e construir algumas lentes ingênuas nós mesmos, apenas por uma questão de aprendizado:

 // Funções puras para visualizar e definir quais podem ser usadas com qualquer lente: 
const view = (lente, loja) => lens.view (loja);
const set = (lente, valor, loja) => lens.set (valor, loja);
 // Uma função que usa prop e retorna ingênua 
// acessadores de lentes para esse prop.
const lensProp = prop => ({
view: store => loja [prop],
// Isso é muito ingênuo, porque funciona apenas para objetos:
set: (valor, loja) => ({
...loja,
[prop]: value
})
});
 // Um objeto de loja de exemplo. Um objeto que você acessa com uma lente 
// é frequentemente chamado de objeto "store":
const fooStore = {
um: 'foo',
b: 'bar'
};
 const aLens = lensProp ('a'); 
const bLens = lensProp ('b');
 // Destrua os suportes `a` e` b` da lente usando 
// a função `view ()`.
const a = view (aLens, fooStore);
const b = view (bLens, fooStore);
console.log (a, b); // 'foo' 'bar'
 // Defina um valor em nossa loja usando o `aLens`: 
const bazStore = set (aLens, 'baz', fooStore);
 // Veja o valor recém-definido. 
console.log (view (aLens, bazStore)); // 'baz'

Vamos provar as leis de lentes para estas funções:

 const store = fooStore; 
 { 
// `view (lens, set (lente, valor, loja))` = `valor`
// Se você definir um valor no armazenamento e imediatamente
// veja o valor através da lente, você obtém o valor
// isso foi definido.
const lens = lensProp ('a');
valor const = 'baz';
 const a = valor; 
const b = view (lente, conjunto (lente, valor, loja));
 console.log (a, b); // 'baz' 'baz' 
}
 { 
// set (lens, b, set (lente, loja)) = set (lens, b, store)
// Se você definir um valor de lente para `a` e, em seguida, definir imediatamente o valor da lente para` b`,
// é o mesmo que se você tivesse definido o valor para `b`.
const lens = lensProp ('a');
 const a = 'bar'; 
const b = 'baz';
 const r1 = set (lente, b, conjunto (lente, a, loja)); 
const r2 = set (lente, b, loja);

console.log (r1, r2); // {a: "baz", b: "barra"} {a: "baz", b: "barra"}
}
 { 
// `set (lente, visão (lente, loja), loja)` = `store`
// Se você obtiver o valor da lente da loja e, em seguida, definir imediatamente esse valor
// de volta para a loja, o valor é inalterado.
const lens = lensProp ('a');
 const r1 = set (lente, visão (lente, loja), loja); 
const r2 = loja;

console.log (r1, r2); // {a: "foo", b: "bar"} {a: "foo", b: "bar"}
}

Compor Lentes

As lentes são compostas. Quando você compõe lentes, a lente resultante mergulhará profundamente no objeto, percorrendo o caminho completo do objeto. Vamos importar o lensProp mais completo, do Ramda para demonstrar:

 import {compose, lensProp, view} de 'ramda'; 
 const lensProps = [ 
'foo',
'Barra',
1
];
 lentes const = lensProps.map (lensProp); 
verdade const = compor (... lentes);
 const obj = { 
foo: {
bar: [falso, verdadeiro]
}
};
 console.log ( 
ver (verdade, obj)
);

Isso é ótimo, mas há mais a composição com lentes que devemos estar cientes. Vamos dar um mergulho mais profundo.

Sobre

É possível aplicar uma função de a => b no contexto de qualquer tipo de dado de functor. Nós já demonstramos que mapeamento de functor é composição. Da mesma forma, podemos aplicar uma função ao valor do foco em uma lente. Normalmente, esse valor seria do mesmo tipo, então seria uma função de a => a . A operação do mapa da lente é comumente chamada de "over" nas bibliotecas JavaScript. Podemos criar assim:

 // over = (lente, f: a => a, loja) => loja 
const over = (lente, f, loja) => set (lente, f (vista (lente, loja)), loja);
 const maiúsculas = x => x.toUpperCase (); 
 console.log ( 
over (aLens, maiúsculas, loja) // {a: "FOO", b: "bar"}
);

Setters obedecem as leis do functor:

 {// se você mapear a função de identidade em uma lente 
// a loja está inalterada.
const id = x => x;
lente constante = aLens;
const a = over (lente, id, loja);
const b = armazenar;
 console.log (a, b); 
}

Para o exemplo de composição, vamos usar uma versão auto-curry de:

 import {curry} de 'ramda'; 
 const over = caril ( 
(lente, f, loja) => set (lente, f (vista (lente, loja)), loja)
);

Agora é fácil ver que as lentes sob a operação over também obedecem à lei de composição do functor:

 {// over (lens, f) após over (lente g) é o mesmo que 
// over (lente, compor (f, g))
lente constante = aLens;
 const store = { 
um: 20
};
 const g = n => n + 1; 
const f = n => n * 2;
 const a = compor ( 
over (lente, f),
over (lente, g)
);
 const b = over (lente, compor (f, g)); 
 console.log ( 
a (loja), // {a: 42}
b (loja) // {a: 42}
);
}

Nós mal arranhamos a superfície das lentes aqui, mas deve ser o suficiente para você começar. Para muito mais detalhes, Edward Kmett falou muito sobre o assunto, e muitas pessoas escreveram muito mais explorações em profundidade.