Caril e composição de funções

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

Nota: Esta é parte da “Compor Software” serie s (agora um livro!) Sobre a aprendizagem de programação funcional e técnicas de software de composição em JavaScript ES6 + a partir do zero. Fique ligado. Há muito mais disso por vir!
<Anterior | << Começar de novo na Parte 1

Com o aumento dramático da programação funcional no JavaScript mainstream, funções curried tornaram-se comuns em muitas aplicações. É importante entender o que eles são, como funcionam e como usá-los bem.

O que é uma função curry?

Uma função curried é uma função que aceita vários argumentos, um de cada vez. Dada uma função com 3 parâmetros, a versão curry receberá um argumento e retornará uma função que recebe o próximo argumento, que retorna uma função que recebe o terceiro argumento. A última função retorna o resultado da aplicação da função a todos os seus argumentos.

Você pode fazer a mesma coisa com mais ou menos parâmetros. Por exemplo, dados dois números, a e b em forma de avaliar num, voltar a soma de a e b :

 // add = a => b => Number 
const add = a => b => a + b;

Para usá-lo, devemos aplicar ambas as funções, usando a sintaxe do aplicativo de função. Em JavaScript, os parênteses () após a referência da função acionam a chamada de função. Quando uma função retorna outra função, a função retornada pode ser invocada imediatamente adicionando um conjunto extra de parênteses:

 const result = add (2) (3); // => 5 

Primeiro, a função recebe a e retorna uma nova função, que então toma b retorna a soma de a e b . Cada argumento é levado um de cada vez. Se a função tiver mais parâmetros, ela poderá simplesmente continuar a retornar novas funções até que todos os argumentos sejam fornecidos e o aplicativo possa ser concluído.

A função add recebe um argumento e, em seguida, retorna um aplicativo parcial de si mesmo com a fixo no escopo de fechamento. Um encerramento é uma função empacotada com seu escopo léxico. Fechamentos são criados no tempo de execução durante a criação da função. Corrigido significa que as variáveis são valores atribuídos no escopo empacotado do fechamento.

Os parênteses no exemplo acima representam invocações de função: add é invocado com 2 , que retorna uma função parcialmente aplicada com a fixo para 2 . Em vez de atribuir o valor de retorno a uma variável ou de outra forma de usá-lo, nós imediatamente invocar a função retornou ao passar 3 a ele entre parênteses, que completa a aplicação e retorna 5 .

O que é uma aplicação parcial?

Uma aplicação parcial é uma função que foi aplicada a alguns, mas ainda não a todos os seus argumentos. Em outras palavras, é uma função que possui alguns argumentos fixados dentro de seu escopo de fechamento. Uma função com alguns de seus parâmetros fixos é considerada parcialmente aplicada .

Qual é a diferença?

Aplicativos parciais podem levar tantos ou poucos argumentos por hora, conforme desejado. Por outro lado, as funções curried retornam sempre uma função unária: uma função que recebe um argumento.

Todas as funções curried retornam aplicações parciais, mas nem todas as aplicações parciais são o resultado de funções curried.

O requisito unário para funções curried é uma característica importante.

O que é estilo livre de pontos?

O estilo livre de pontos é um estilo de programação onde as definições de funções não fazem referência aos argumentos da função. Vamos ver as definições de funções em JavaScript:

 função foo (/ * parâmetros são declarados aqui * /) { 
// ...
}
 const foo = (/ * parâmetros são declarados aqui * /) => // ... 
 const foo = function (/ * parâmetros são declarados aqui * /) { 
// ...
}

Como você pode definir funções em JavaScript sem fazer referência aos parâmetros necessários? Bem, não podemos usar a palavra-chave function , e não podemos usar uma função de seta ( => ) porque eles exigem que qualquer parâmetro formal seja declarado (o que referenciaria seus argumentos). Então, o que precisamos fazer é chamar uma função que retorna uma função.

Crie uma função que incrementa qualquer número que você passar para ela usando um estilo sem ponto. Lembre-se, nós já temos uma função chamada add que pega um número e retorna uma função parcialmente aplicada com seu primeiro parâmetro fixo para o que você passar. Podemos usar isso para criar uma nova função chamada inc() :

 // inc = n => Number 
// Adiciona 1 a qualquer número.
const inc = adicionar (1);
 inc (3); // => 4 

Isso se torna interessante como mecanismo de generalização e especialização. A função retornada é apenas uma versão especializada da função add() mais geral. Podemos usar add() para criar quantas versões especializadas quisermos:

 const inc10 = adicionar (10); 
const inc20 = adicionar (20);
 inc10 (3); // => 13 
inc20 (3); // => 23

E, claro, todos eles têm seus próprios escopos de fechamento (os fechamentos são criados no momento da criação da função – quando o add() é invocado), então o inc() continua funcionando:

 inc (3) // 4 

Quando criamos inc() com a chamada de função add(1) , a a parâmetro dentro add() fica fixo a 1 dentro da função retornou que é atribuído a inc .

Então, quando chamamos inc(3) , o parâmetro b dentro de add() é substituído pelo valor do argumento, 3 , e o aplicativo é concluído, retornando a soma de 1 e 3 .

Todas as funções curried são uma forma de função de ordem superior que permite criar versões especializadas da função original para o caso de uso específico em questão.

Por que nos curry?

Funções curadas são particularmente úteis no contexto da composição da função.

Na álgebra, dadas duas funções, f e g :

 f: a -> b 
g: b -> c

Você pode compor essas funções juntas para criar uma nova função, h de a diretamente para c :

 // Algebra definition, borrowing the `.` composition operator 
// from Haskell
 h: a -> c 
h = f . g = f(g(x))

Em JavaScript:

 const g = n => n + 1; 
const f = n => n * 2;
 const h = x => f (g (x)); 
 h (20); // => 42 

A definição da álgebra:

 f . g = f(g(x)) 

Pode ser traduzido em JavaScript:

 Const Compor = (f, g) => x => f (g (x)); 

Mas isso só seria capaz de compor duas funções de cada vez. Na álgebra, é possível escrever:

 f . g . h 

Podemos escrever uma função para compor quantas funções quiser. Em outras palavras, compose() cria um pipeline de funções com a saída de uma função conectada à entrada da próxima.

Aqui está a maneira que eu costumo escrever:

 const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x); 

Essa versão usa qualquer número de funções e retorna uma função que recebe o valor inicial e, em seguida, usa reduceRight() para iterar da direita para esquerda sobre cada função, f , em fns e aplicá-la ao valor acumulado, y . O que estamos acumulando com o acumulador, y nessa função é o valor de retorno para a função retornada por compose() .

Agora podemos escrever nossa composição assim:

 const g = n => n + 1; 
const f = n => n * 2;
 // substitua `x => f (g (x))` com `compose (f, g)` 
const h = compor (f, g);
 h (20); // => 42 

Vestígio

A composição de funções usando o estilo livre de pontos cria um código muito conciso e legível, mas pode custar uma depuração fácil. E se você quiser inspecionar os valores entre as funções? trace() é um utilitário prático que permitirá que você faça exatamente isso. Toma a forma de uma função curry:

 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};

Agora podemos inspecionar o pipeline:

 const compose = (... fns) => x => fns.reduceRight ((y, f) => f (y), x); 
 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 / * 
Nota: a ordem da aplicação da função é
de baixo para cima:
* /
const h = compor (
trace ('after f'),
f,
trace ('after g'),
g
);
 h (20); 
/ *
depois de g: 21
depois de f: 42
* /

compose() é um ótimo utilitário, mas quando precisamos compor mais de duas funções, às vezes é útil se podemos lê-las na ordem de cima para baixo. Podemos fazer isso invertendo a ordem em que as funções são chamadas. Há outro utilitário de composição chamado pipe() que compõe em ordem reversa:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 

Agora podemos escrever o código acima assim:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 / * 
Agora a ordem da aplicação da função
funciona de cima para baixo:
* /
const h = pipe (
g,
trace ('after g'),
f,
trace ('after f'),
);
 h (20); 
/ *
depois de g: 21
depois de f: 42
* /

Curry e Composição Funcional, Juntos

Mesmo fora do contexto da composição de funções, o currying é certamente uma abstração útil que podemos usar para especializar funções. Por exemplo, uma versão curry de map() pode ser especializada para fazer muitas coisas diferentes:

 mapa de const = fn => mappable => mappable.map (fn); 
 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
const log = (... args) => console.log (... args);
 const arr = [1, 2, 3, 4]; 
const isEven = n => n% 2 === 0;
 listra const = n => isEven (n)? 'luz negra'; 
const stripeAll = map (stripe);
const striped = stripeAll (arr);
log (listrado);
// => ["light", "dark", "light", "dark"]
 const double = n => n * 2; 
const doubleAll = map (duplo);
const dobrado = doubleAll (arr);
log (duplicado);
// => [2, 4, 6, 8]

Mas o verdadeiro poder das funções curried é que elas simplificam a composição das funções. Uma função pode receber qualquer número de entradas, mas pode retornar apenas uma única saída. Para que as funções possam ser compostas, o tipo de saída deve estar alinhado com o tipo de entrada esperado:

 f: a => b 
g: b => c
h: a => c

Se a função g acima dos dois parâmetros esperados, a saída de f não se alinharia com a entrada para g :

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

Como podemos obter x em g neste cenário? Normalmente, a resposta é para curry g .

Lembre-se de que a definição de uma função curried é uma função que usa vários parâmetros, um de cada vez , pegando o primeiro argumento e retornando uma série de funções, cada uma pegando o próximo argumento até que todos os parâmetros tenham sido coletados.

As palavras-chave nessa definição são “uma por vez”. A razão pela qual as funções curried são tão convenientes para a composição de funções é que elas transformam funções que esperam múltiplos parâmetros em funções que podem assumir um único argumento, permitindo que elas se encaixem em um pipeline de composição de funções. Tome a função trace() como um exemplo, do anterior:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 const h = pipe ( 
g,
trace ('after g'),
f,
trace ('after f'),
);
 h (20); 
/ *
depois de g: 21
depois de f: 42
* /

trace() define dois parâmetros, mas os usa um de cada vez, permitindo especializar a função inline. Se trace() não estivesse curry, não poderíamos usá-lo dessa maneira. Teríamos que escrever o pipeline assim:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
 const trace = (label, value) => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 const h = pipe ( 
g,
// as chamadas trace () não são mais pontuais,
// introduzindo a variável intermediária, `x`.
x => trace ('depois de g', x),
f,
x => trace ('after f', x),
);
 h (20); 

Mas simplesmente curry uma função não é suficiente. Você também precisa garantir que a função esteja esperando parâmetros na ordem correta para especializá-los. Veja o que acontece se currymos o trace() novamente, mas invertemos a ordem dos parâmetros:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
 const trace = value => label => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 const h = pipe ( 
g,
// as chamadas trace () não podem ser livres de pontos
// porque os argumentos são esperados na ordem errada.
x => traço (x) ('depois de g'),
f,
x => traço (x) ('depois de f'),
);
 h (20); 

Se você está em apuros, pode corrigir esse problema com uma função chamada flip() , que simplesmente inverte a ordem de dois parâmetros:

 const flip = fn => a => b => fn (b) (a); 

Agora podemos criar uma função flippedTrace() :

 const flippedTrace = inverter (traçar); 

E usá-lo assim:

 const flip = fn => a => b => fn (b) (a); 
pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x);
 const trace = value => label => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
const flippedTrace = inverter (traçar);
 const g = n => n + 1; 
const f = n => n * 2;
 const h = pipe ( 
g,
flippedTrace ('after g'),
f,
FlippedTrace ('after f'),
);
 h (20); 

Mas uma abordagem melhor é escrever a função corretamente em primeiro lugar. Às vezes, o estilo é chamado de "data last" (últimos dados), o que significa que você deve primeiro tomar os parâmetros especializados e obter os dados sobre os quais a função atuará por último. Isso nos dá a forma original da função:

 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};

Cada aplicativo de trace() para um label cria uma versão especializada da função de rastreio usada no pipeline, em que o rótulo é corrigido dentro do aplicativo parcial de trace retornado. Então, é isso:

 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const traceAfterG = trace ('depois de g'); 

… É equivalente a isto:

 const traceAfterG = value => { 
label const = 'depois de g';
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};

Se trocássemos trace('after g') por traceAfterG , isso significaria a mesma coisa:

 pipe const = (... fns) => x => fns.reduce ((y, f) => f (y), x); 
 const trace = label => value => { 
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 // A versão curry do trace () 
// nos salva de escrever todo esse código ...
const traceAfterG = value => {
label const = 'depois de g';
console.log (`$ {label}: $ {valor}`);
valor de retorno;
};
 const g = n => n + 1; 
const f = n => n * 2;
 const h = pipe ( 
g,
traceAfterG,
f,
trace ('after f'),
);
 h (20); 

Conclusão

Uma função curried é uma função que usa vários parâmetros, um de cada vez, obtendo o primeiro argumento e retornando uma série de funções, cada qual pegando o próximo argumento até que todos os parâmetros tenham sido corrigidos e o aplicativo de função possa ser concluído. ponto, o valor resultante é retornado.

Uma aplicação parcial é uma função que já foi aplicada a alguns – mas ainda não todos – de seus argumentos. Os argumentos aos quais a função já foi aplicada são chamados de parâmetros fixos .

O estilo sem pontos é uma maneira de definir uma função sem referência a seus argumentos. Geralmente, uma função sem ponto é criada chamando uma função que retorna uma função, como uma função curry.

As funções curry são ótimas para a composição de funções, pois permitem converter facilmente uma função n-ária na forma de função unária necessária para os pipelines de composição de função: As funções em um pipeline devem esperar exatamente um argumento.

As últimas funções de dados são convenientes para a composição das funções, porque podem ser facilmente utilizadas em estilo sem pontos.

Próximos passos

Um vídeo completo passo a passo disso está disponível para os membros do EricElliottJS.com . Membros, visite a lição ES6 Curry & Composition .