O conceito é simples: você pode chamar uma função com menos argumentos do que espera, então ela retorna uma nova função que recebe os argumentos que faltam.
Você pode escolher se vai passar todos os argumentos de uma vez, ou um por vez.
var add = function(x) {
return function(y) {
return x + y
}
}
// ou
const add = x => y => x + y
const increment = add(1)
const addTen = add(10)
increment(2)
// 3
addTen(2)
// 12
Aqui fizemos uma função add
que recebe um argumento e retorna uma função. Ao chamá-la, a função retornada lembra do primeiro argumento por closure. Chamando ela com ambos os argumentos não funciona, porém podemos utilizar uma função para facilitar isso, chamada curry
.
const { curry } = require('ramda')
const match = curry(
(what, str) => str.match(what)
)
const replace = curry(
(what, replacement, str) => str.replace(what, replacement)
)
const filter = curry(
(fn, array) => array.filter(fn)
)
const map = curry(
(fn, array) => array.map(fn)
)
Se você observar, o padrão usado nas funções acima é de receber os dados os quais estamos realizando uma operação (String, Array) no último argumento. Isso tem um motivo e vai ficar mais claro o porque desse formato.
match(/\s+/g, 'hello world')
// [ ' ' ]
match(/\s+/g)('hello world')
// [ ' ' ]
const hasSpaces = match(/\s+/g)
// function(x) { return x.match(/\s+/g) }
// ou
// x => x.match(/\s+/g)
hasSpaces('hello world')
// [ ' ' ]
hasSpaces('spaceless')
// null
filter(hasSpaces, ['tori_spelling', 'tori amos'])
// ['tori amos']
const findSpaces = filter(hasSpaces)
// function(xs) { return xs.filter(function(x) { return x.match(/\s+/g) }) }
// ou
// xs => xs.filter(x => x.match(/\s+/g))
findSpaces(['tori_spelling', 'tori amos'])
// ['tori amos']
const noVowels = replace(/[aeiouy]/ig)
// function(replacement, x) { return x.replace(/[aeiouy]/ig, replacement) }
// ou
// (replacement, x) => x.replace(/[aeiouy]/ig, replacement)
const censored = noVowels("*")
// function(x) { return x.replace(/[aeiouy]/ig, '*') }
// ou
// x => x.replace(/[aeiouy]/ig, '*')
censored('Chocolate Rain')
// 'Ch*c*l*t* R**n'
O que está sendo demonstrado aqui é a habilidade de "carregar" uma função com um argumento ou dois para receber uma nova função que lembra desses argumentos.
Currying é util para várias coisas. Nós conseguimos fazer novas funções apenas dando a nossas funções base alguns argumentos, como é possível ser visto em hasSpaces
, findSpaces
e censored
.
Nós também temos a habilidade de transformar quaisquer funções que funcionam em únicos elementos, em uma função que pode ser aplicada em arrays simplesmente a envolvendo com um map
:
const { map } = require('ramda')
const getProp = propName => obj => obj[propName]
const getAllIds = map(getProp('id'))
const people = [
{ id: 1, name: 'maria' },
{ id: 2, name: 'john' },
{ id: 3, name: 'davis' },
]
getAllIds(people)
// [ 1, 2, 3 ]
const monsters = [
{ id: 23, name: 'godzilla' },
{ id: 42, name: 'king kong' },
]
getAllIds(monsters)
// [ 23, 42 ]
Passando uma função com menos argumentos do que espera é tipicamente chamado de partial application. Aplicando parcialmente uma função pode remover muito código desnecessário. Considere que a função map
não fosse curried, seria necessário passar a função que é aplicada em cada elemento do array toda vez.
const peopleIds = map(person => getProp('id', person), people)
const monstersIds = map(monster => getProp('id', monster), monsters)
Nós tipicamente não definimos funções que envolvem arrays, porque podemos simplesmente criar uma que funciona em um item e aplicar um map(nossaFuncao)
. O mesmo ocorre com sort
, filter
e outras funções de alta ordem.
Quando falamos sobre funções puras, dissemos que elas recebem 1 entrada e produzem 1 saída. Currying não faz exatamente isso: cada argumento retorna uma nova função esperando o resto dos argumentos. Isso sim é 1 entrada para 1 saída.
Não importa se a saída é uma outra função - ela é qualificada como pura. Nós permitimos mais de um argumento por vez, mas isso é visto como meramente conveniente ao deixar de ter de chamar uma função com um ()
para cada parametro.
Currying é muito útil e é uma ferramenta que faz programação funcional ser menos verbosa.
Nós podemos fazer novas funções úteis on the fly
apenas passando menos argumentos e como bônus, teremos retido a definição da função independente do número de argumentos.
Próximo capítulo será sobre outra ferramenta essencial chamada compose
.
Pequenas notas antes de começar.
Vamos usar Ramda, pois suas funções já são curried por padrão. E também porque possui a função curry
que irá facilitar na criação de novas funções curryable.
Existem alguns testes unitários para rodar contra suas implementações dos exercícios (https://github.com/DrBoolean/mostly-adequate-guide/tree/master/code/part1_exercises)
As respostas estão no repo do livro (https://github.com/DrBoolean/mostly-adequate-guide/tree/master/code/part1_exercises/answers)
const { split, map, filter, match, reduce } = require('ramda')
// Exercício 1
//==============
// Refatore
var words = function(str) {
return split(' ', str)
}
// Exercício 1a
//==============
// Crie uma função que funcione em um array de strings.
var sentences = undefined
// Exercício 2
//==============
// Refatore
var filterQs = function(xs) {
return filter(function(x) {
return match(/q/i, x)
}, xs)
}
// Exercício 3
//==============
// Usar função keepHighest para refatorar max
// DEIXE ASSIM / NÃO MEXER:
var keepHighest = function(x, y) {
return x >= y ? x : y
}
// REFATORE ESSA
var max = function(xs) {
return reduce(function(acc, x) {
return keepHighest(acc, x)
}, -Infinity, xs)
}
// Bônus 1:
// ============
// Implementar `slice` para que seja funcional e curried
// //[1, 2, 3].slice(0, 2)
var slice = undefined
// Bônus 2:
// ============
// Use slice para definir uma função "take" que retorna n elementos do início do array. Tem de ser curried.
// Para ['a', 'b', 'c'] com n=2 deveria retornar ['a', 'b'].
var take = undefined