Compor em todos os lugares: composição de funções em JavaScript

A tradução do artigo foi preparada especialmente para os alunos do curso Desenvolvedor de JavaScript.Básico .










Introdução



Parece que as bibliotecas Lodash e Underscore agora são onipresentes e ainda têm o método de composição supereficiente que conhecemos hoje .



Vamos dar uma olhada mais de perto na função de composição e ver como ela pode tornar seu código mais legível, sustentável e elegante.



O básico



Cobriremos muitas funções Lodash porque 1) não vamos escrever nossos próprios algoritmos básicos - isso vai nos distrair do que sugiro focar; e 2) a biblioteca Lodash é usada por muitos desenvolvedores e pode ser substituída por Underscore, qualquer outra biblioteca ou seus próprios algoritmos sem problemas.



Antes de examinar alguns exemplos simples, vamos examinar exatamente o que a função de composição faz e como implementar sua própria função de composição, se necessário.



var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};


Esta é a implementação mais básica. Observe que as funções em argumentos serão executadas da direita para a esquerda . Ou seja, a função à direita é executada primeiro e seu resultado é passado para a função à esquerda dela.



Agora vamos dar uma olhada neste código:



function reverseAndUpper(str) {
  var reversed = reverse(str);
  return upperCase(reversed);
}


A função reverseAndUpper primeiro inverte a string especificada e a converte em maiúsculas. Podemos reescrever este código usando a função de composição básica:



var reverseAndUpper = compose(upperCase, reverse);


A função reverseAndUpper agora pode ser usada:



reverseAndUpper(''); // 


Poderíamos ter obtido o mesmo resultado escrevendo o código:



function reverseAndUpper(str) {
  return upperCase(reverse(str));
}


Essa opção parece mais elegante e é mais fácil de manter e reutilizar.



A capacidade de projetar funções rapidamente e criar pipelines de processamento de dados será útil em várias situações quando você precisar transformar dados em trânsito. Agora imagine que você precisa passar uma coleção para uma função, transformar os elementos da coleção e retornar o valor máximo após todas as etapas do pipeline terem sido concluídas ou converter uma determinada string em um valor booleano. A função de composição permite combinar várias funções e, assim, criar uma função mais complexa.



Vamos implementar uma função de composição mais flexível que pode incluir qualquer número de outras funções e argumentos. A função de composição anterior que examinamos funciona apenas com duas funções e recebe apenas o primeiro argumento passado. Podemos reescrever assim:



var compose = function() {
  var funcs = Array.prototype.slice.call();
 
  return funcs.reduce(function(f,g) {
    return function() {
      return f(g.apply(this, ));
    };
  });
};


Com uma função como esta, podemos escrever código como este:



Var doSometing = compose(upperCase, reverse, doSomethingInitial);
 
doSomething('foo', 'bar');


Existem muitas bibliotecas que implementam a função de composição. Precisamos de nossa função de composição para entender como funciona. Claro, ele é implementado de forma diferente em diferentes bibliotecas. As funções de composição de diferentes bibliotecas fazem a mesma coisa, portanto, são intercambiáveis. Agora que entendemos do que se trata a função de composição, vamos dar uma olhada na função _.compose da biblioteca Lodash com os exemplos a seguir.



Exemplos de



Vamos começar de forma simples:



function notEmpty(str) {
    return ! _.isEmpty(str);
}


A função notEmpty é a negação do valor retornado pela função _.isEmpty.



Podemos obter o mesmo resultado usando a função _.compose da biblioteca Lodash. Vamos escrever a função não:



function not(x) { return !x; }
 
var notEmpty = _.compose(not, _.isEmpty);


Agora você pode usar a função notEmpty com qualquer argumento:



notEmpty('foo'); // true
notEmpty(''); // false
notEmpty(); // false
notEmpty(null); // false


Este é um exemplo muito simples. Vamos dar uma olhada em algo

um pouco mais complicado: a função findMaxForCollection retorna o valor máximo de uma coleção de objetos com as propriedades id e val (valor).



function findMaxForCollection(data) {
    var items = _.pluck(data, 'val');
    return Math.max.apply(null, items);
}
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data);


A função de composição pode ser usada para resolver este problema:



var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck);
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data, 'val'); // 6


Há trabalho a fazer aqui.



_.pluck espera uma coleção como o primeiro argumento e uma função de retorno de chamada como o segundo. É possível aplicar parcialmente a função _.pluck? Nesse caso, você pode usar currying para alterar a ordem dos argumentos.



function pluck(key) {
    return function(collection) {
        return _.pluck(collection, key);
    }
}


A função findMaxForCollection precisa ser ajustada um pouco mais. Vamos criar nossa própria função max.



function max(xs) {
    return Math.max.apply(null, xs);
}


Agora podemos tornar nossa função de composição mais elegante:



var findMaxForCollection = _.compose(max, pluck('val'));
 
findMaxForCollection(data);


Escrevemos nossa própria função de arrancar e só podemos usá-la com a propriedade 'val'. Você pode não entender porque você deve escrever seu próprio método de busca se Lodash já tem uma função pronta e conveniente _.pluck. O problema é que ele _.pluckespera uma coleção como o primeiro argumento e queremos fazer isso de forma diferente. Alterando a ordem dos argumentos, podemos aplicar parcialmente a função, passando a chave como o primeiro argumento; a função retornada aceitará dados.

Podemos refinar nosso método de amostragem um pouco mais. Lodash tem um método prático _.curryque permite escrever nossa função assim:



function plucked(key, collection) {
    return _.pluck(collection, key);
}
 
var pluck = _.curry(plucked);


Apenas envolvemos a função original de arrancar para trocar os argumentos. Agora, pluck retornará a função até que tenha processado todos os argumentos. Vamos ver como fica o código final:



function max(xs) {
    return Math.max.apply(null, xs);
}
 
function plucked(key, collection) {
    return _.pluck(collection, key);
}
 
var pluck = _.curry(plucked);
 
var findMaxForCollection = _.compose(max, pluck('val'));
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data); // 6


A função findMaxForCollection pode ser lida da direita para a esquerda, ou seja, primeiro a propriedade val de cada item da coleção é passada para a função e, em seguida, retorna o máximo de todos os valores aceitos.



var findMaxForCollection = _.compose(max, pluck('val'));


Esse código é mais fácil de manter, reutilizável e muito mais elegante. Vejamos o último exemplo, apenas para desfrutar mais uma vez da elegância da composição de funções.



Vamos pegar os dados do exemplo anterior e adicionar uma nova propriedade chamada ativa. Agora, os dados têm a seguinte aparência:



var data = [{id: 1, val: 5, active: true}, 
            {id: 2, val: 6, active: false }, 
            {id: 3, val: 2, active: true }];




Vamos chamar essa função getMaxIdForActiveItems (data) . Ele pega uma coleção de objetos, filtra todos os objetos ativos e retorna o valor máximo dos filtrados.



function getMaxIdForActiveItems(data) {
    var filtered = _.filter(data, function(item) {
        return item.active === true;
    });
 
    var items = _.pluck(filtered, 'val');
    return Math.max.apply(null, items);
}


Você pode tornar este código mais elegante? Ele já tem as funções max e pluck, então só precisamos adicionar um filtro:



var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter);
 
getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5


_.filterO mesmo problema surge com a função e com _.pluck: não podemos aplicar parcialmente esta função, porque ela espera uma coleção como o primeiro argumento. Podemos alterar a ordem dos argumentos no filtro envolvendo a função inicial:



function filter(fn) {
    return function(arr) {
        return arr.filter(fn);
    };
}


Vamos adicionar uma função isActiveque pega um objeto e verifica se o sinalizador ativo está definido como verdadeiro.



function isActive(item) {
    return item.active === true;
}


Uma função filtercom uma função isActivepode ser parcialmente aplicada, portanto , apenas passaremos os dados para a função getMaxIdForActiveItems .



var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive));


Agora só precisamos transferir os dados:



getMaxIdForActiveItems(data); // 5


Agora, nada nos impede de reescrever essa função para que ela encontre o valor máximo entre os objetos inativos e o retorne:



var isNotActive = _.compose(not, isActive);

var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));


Conclusão



Como você deve ter notado, a composição de funções é um recurso interessante que torna seu código elegante e reutilizável. O principal é aplicá-lo corretamente.



Links



Lodash

Ei, sublinhado, você está fazendo isso errado! (Ei, sublinhado, você está fazendo tudo errado!)

@sharifsbeat







Consulte Mais informação:






All Articles