JavaScript: Currying
Currying é o processo de transformar uma função que espera vários argumentos em uma função que espera um único argumento e retorna outra função curried. Por exemplo, uma função que recebe três argumentos, ao sofrer currying, resulta em uma função que recebe um argumento e retorna uma função que recebe um argumento, que por sua vez retorna uma função que recebe um argumento e retorna o resultado da função original.
Aplicação parcial de uma função corresponde a chamar a função passando menos argumentos do que a função recebe.
Considere a função produto
:
function produto(a, b) {
return a * b;
}
Do jeito que a função está definida, não faz sentido realizar uma aplicação parcial:
A chamada produto(2)
equivale à chamada produto(2, undefined)
, resultado na multiplicação 2 * undefined
, cujo resultado é NaN
(not a number).
Usando closures para implementar funções curried
Uma versão curried da função (i.e., função resultante do currying) pode ser escrita usando-se closures:
Outra forma de escrever:
Exercício básico de currying
Vamos praticar? Considere a seguinte função de 3 argumentos, que calcula o valor de uma função de primeiro grau em determinado ponto:
Vamos escrever uma versão curried dessa função? Essa versão deve receber a
e retornar uma função que recebe b
, que retorna uma função que recebe x
e retorna a resposta.
Aplicação prática de currying: conversor de temperaturas
Dá pra ver que, na versao curried, a maneira de chamar a função fica diferente: usamos (3)(2)(42)
em vez de (3, 2, 42)
. Isso porque cada chamada, exceto a última, retorna uma função, e por isso precisamos fazer 3 chamadas em vez de uma.
A vantagem de escrever uma função curried é que fica mais fácil construir funções com base nas funções curried fixando alguns argumentos. Por exemplo, a função celsiusParaFahrenheit
pode ser escrita assim:
Até agora nós realizamos o currying das funções de forma manual, reescrevendo o código das funções. Em algumas linguagens, como Haskell, todas as funções são curried (e, portanto, admitem aplicação parcial). Isso não acontece com Javascript, mas há bibliotecas capazes de realizar o currying de qualquer função.
A biblioteca Ramda
A biblioteca Ramda possui uma função, R.curry
, que recebe uma função e retorna uma versão curried dessa função.
A biblioteca possui outras funções, como R.map
, R.filter
, R.reduce
e R.length
. Todas as funções da biblioteca Ramda já são curried e, portanto, admitem aplicação parcial.
O currying feito pela biblioteca Ramda é mais sofisticado, pois permite uma aplicação parcial com um, dois ou mais argumentos, retornando uma função que recebe os argumentos restantes:
Aplicação prática: uso com map
Currying é útil em conjunção de funções de alta ordem. Considere a função arr.map(f)
, que retorna um array resultante da aplicação da função f
a cada elemento de arr
. A função map
chama f
passando um único argumento; como podemos então usar nossa função primeiroGrau
para mapear elementos de um array? Usando currying:
Ordem dos parâmetros
Se você quer criar funções que podem ser curried, vale a pena pensar na ordem dos seus argumentos. O ideal é deixar concentrar os argumentos que tendem a ser fixados no início, e deixar os argumentos que variam mais no final da lista de argumentos. Note o exemplo da função primeiroGrau(a, b, x)
: normalmente, quando chamamos a função várias vezes, fixamos a
e b
e variamos x
; por isso x
foi colocado como último argumento.
Composição de funções
(Baseado em https://medium.com/@collardeau/intro-to-functional-programming-concepts-in-javascript-b0650773139c e https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch5.html)
Considere o problema de obter a última inicial do nome de uma pessoa. Podemos decompor esse problema em dois: obter o último nome de uma pessoa e obter a primeira letra de um nome:
function getUltimoNome(nomeCompleto) { return nomeCompleto.split(" ").splice(-1)[0]; }
function getPrimeiraLetra(string) { return string[0]; }
A função que retorna a última inicial de um nome pode então ser escrita como uma composição das funções getUltimoNome
e getPrimeiraLetra
:
Lembre-se do conceito de composição de funções da matemática: se eu tenho as funções f(x), g(x) e h(x), a composição dessas funções, na ordem apresentada, é uma nova função, c(x) = h(g(f(x))).
A biblioteca Ramda fornece uma função de alta ordem para compor duas ou mais funções quaisquer: R.pipe
. No exemplo da matemática, c(x) = R.pipe(f, g, h). O exemplo da última inicial de um nome fica assim:
(shell scripting também usa o conceito de pipe)
Esse estilo de programar é chamado de pointfree, pois a função getUltimaInicial
é definida sem declarar quais são os seus parâmetros.
Exercício de composição de funções
Agora é com você. Crie uma função para retornar a quantidade de elementos de um array cujo quadrado é ímpar.
Você pode ignorar o fato de que o quadrado de um número é ímpar se, e somente se o número é impar.
Composição de funções e número de parâmetros
Na composição de funções, o retorno da primeira função é passado como parâmetro na chamada da segunda função, o retorno da segunda função é passado como parâmetro na chamada da terceira função, e assim por diante.
Na composição de funções, todas as funções a partir da segunda devem ser funções de um único parâmetro. Você sabe explicar por quê?
Vamos ver um exemplo agora em que a primeira função da composição recebe mais de um parâmetro.
Por que a primeira função de uma composição pode ser uma função de dois ou mais parâmetros?
Vamos relembrar o exemplo anterior:
A função filtraImpar
é uma função de um parâmetro; por isso ela pode ser usada como segunda função de uma composição. Essa função pode ser escrita de forma mais compacta usando aplicação parcial: filtraImpar = R.filter(x => x % 2 == 1)
(note que R.filter é uma função curried, assim como todas as funções da biblioteca Ramda). Com essa observação, modifique o código abaixo, substituindo /*...*/
por aplicações parciais de R.map
, R.filter
e R.length
, de forma a obter o mesmo comportamento do código acima.
A biblioteca Ramda também possui a função compose
, que faz a mesma coisa que a função pipe
, porém recebendo os argumentos na ordem inversa.
Exercícios: https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch5.html
Curiosidade
Pipes são extremamente úteis para escrever transformações de dados. Na linguagem R, uma biblioteca introduziu o operador de pipe, %>%
, que é comumente usado para aplicar sequências de transformações sobre uma tabela. Para saber mais, leia Simpler R coding with pipes > the present and future of the magrittr package.
Exercício 1
Refatore o código para remover todos os argumentos através da aplicação parcial da função.
Considere a função wordsOriginal
, abaixo, e escreva a função equivalente words
, através da aplicação parcial de R.split
:
Exercício 1a
Use map
para criar uma nova função, wordsArray
, que aplica words a cada elemento da array de entrada.
Exercício 2
A função filterQs
recebe um array de palavras e retorna apenas as palavras que possuem a letra q. Crie uma versão pointfree dessa função, usando composição de funções parcialmente aplicadas:
Exercício 3
Use a função auxiliar _mantemMaior
para criar uma versão pointfree de max
:
Baseado em https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch4.html