1. Primeiros passos
2. Combine as funções
3. Uso parcial (currying)
4. Programação declarativa
5. Notação quintessencial
6. Imutabilidade e objetos
7. Imutabilidade e matrizes
8. Lentes
9. Conclusão
Este post é a sexta parte de uma série de artigos sobre programação funcional chamada Ramda Style Thinking.
Na quinta parte, falamos sobre escrever funções no estilo de notação sem sentido, em que o argumento principal com os dados de nossa função não é especificado explicitamente.
Nesse momento, não podíamos reescrever todas as nossas funções em um estilo sem bits, porque não tínhamos algumas ferramentas necessárias para isso. É hora de estudá-los.
Lendo propriedades do objeto
Vejamos novamente o exemplo da definição de pessoas com direito a voto, que examinamos na quinta parte :
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen)
Como você pode ver, tornamos isCitizen
e isEligibleToVote
, mas não podemos fazer isso com as três primeiras funções.
Como aprendemos na quarta parte , podemos tornar nossas funções mais declarativas através do uso de iguais e gte . Vamos começar com isso:
const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18)
Para tornar essas funções inúteis, precisamos de uma maneira de construir a função para aplicar a variável person
no final da expressão. O problema é que precisamos acessar as propriedades da person
, agora sabemos a única maneira de fazer isso - e isso é imperativo.
sustentar
Felizmente, Ramda mais uma vez vem em nosso auxílio. Ele fornece uma função prop para acessar as propriedades dos objetos.
Usando prop
, podemos reescrever person.birthCountry
em prop('birthCountry', person)
. Vamos fazer isso:
const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
Uau, agora parece muito pior. Mas vamos continuar nossa refatoração. Vamos mudar a ordem dos argumentos que passamos para equals
para que o prop
venha por último. equals
funciona exatamente da mesma maneira ao contrário, por isso não quebramos nada:
const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
Em seguida, vamos usar currying, a propriedade natural de equals
e gte
, a fim de criar novas funções às quais o resultado da chamada prop
será aplicado:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person))
Ainda parece a pior opção, mas ainda vamos continuar. Vamos aproveitar o currying novamente para todas as chamadas de prop
:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person))
Mais uma vez, de alguma forma, não muito. Mas agora vemos um padrão familiar. Todas as nossas funções têm a mesma imagem f(g(person))
e, como sabemos na segunda parte , isso é equivalente a compose(f, g)(person)
.
Vamos aplicar esta vantagem ao nosso código:
const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person)
Agora temos algo. Todas as nossas funções se parecem com person => f(person)
. E já sabemos da quinta parte que podemos tornar essas funções inúteis.
const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age'))
Quando começamos, não era óbvio que nossos métodos fizeram duas coisas. Eles se voltaram para a propriedade do objeto e prepararam algumas operações com seu valor. Essa refatoração em um estilo inútil tornou isso muito explícito.
Vamos dar uma olhada em algumas das outras ferramentas que o Ramda fornece para trabalhar com objetos.
escolher
Onde prop
lê uma propriedade de um objeto e retorna seu valor, pick lê muitas propriedades do objeto e retorna um novo objeto apenas com elas.
Por exemplo, se precisarmos apenas dos nomes e anos das pessoas, podemos usar pick(['name','age'], person)
.
tem
Se queremos apenas saber que nosso objeto tem uma propriedade, sem ler seu valor, podemos usar a função has para verificar suas propriedades, assim como hasIn para verificar a cadeia de protótipos: has('name', person)
.
caminho
Onde prop
uma propriedade de objeto, o caminho vai mais fundo nos objetos aninhados. Por exemplo, queremos extrair o CEP de uma estrutura mais profunda: path(['address','zipCode'], person)
.
Observe que o path
mais indulgente do que prop
. path
retornará undefined
se algo no caminho (incluindo o argumento original) for null
ou undefined
, enquanto prop
causará um erro nessas situações.
propOr / pathOr
propOr e pathOr são semelhantes a prop
e path
combinados com defaultTo
. Eles fornecem a capacidade de especificar um valor padrão para uma propriedade ou caminho que não pode ser encontrado no objeto que está sendo estudado.
Por exemplo, podemos fornecer um espaço reservado quando não sabemos o nome da pessoa: propOr('<Unnamed>, 'name', person)
. Observe que, diferentemente de prop
, propOr
não causará um erro se a person
for null
ou undefined
; em vez disso, ele retornará o valor padrão.
chaves / valores
keys retorna uma matriz contendo todos os nomes de todas as propriedades conhecidas do objeto. Os valores retornarão os valores dessas propriedades. Essas funções podem ser úteis quando combinadas com as funções de iteração para coleções, sobre as quais aprendemos na primeira parte .
Adicionar, atualizar e excluir propriedades
Agora, temos muitas ferramentas para ler objetos em um estilo declarativo, mas e as alterações?
Como a imutabilidade é importante para nós, não queremos modificar objetos diretamente. Em vez disso, queremos retornar novos objetos que foram alterados da maneira que queremos.
Mais uma vez, Ramda nos oferece muitos benefícios.
assoc / assocPath
Quando programamos em um estilo imperativo, podemos definir ou alterar o nome da pessoa através do operador de atribuição: person.name = 'New name'
.
Em nosso mundo imutável e funcional, podemos usar assoc : const updatedPerson = assoc('name', 'newName', person)
.
assoc
retorna um novo objeto com um valor de propriedade adicionado ou atualizado, mantendo o objeto original inalterado.
Também temos à nossa disposição assocPath para atualizar a propriedade anexada: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person)
.
dissoc / dissocPath / omit
E quanto a excluir propriedades? Imperativamente, podemos querer dizer delete person.age
. No Ramda, usaremos dissoc : `const updatedPerson = dissoc ('age', person)
dissocPath é praticamente o mesmo, mas funciona em estruturas mais profundas de objetos: dissocPath(['address', 'zipCode'], person)
.
E também temos omit , que pode remover várias propriedades ao mesmo tempo: const updatedPerson = omit(['age', 'birthCountry'], person)
.
Observe que pick
e omit
pouco semelhantes e se complementam muito bem. Eles são muito convenientes para a lista de permissões (salvar apenas um determinado conjunto de propriedades usando pick
) e listas negras (livrar-se de determinadas propriedades através do uso de omit
).
Agora sabemos o suficiente para trabalhar com objetos em um estilo declarativo e imutável. Vamos escrever uma função celebrateBirthday
que atualize a idade da pessoa no aniversário dela.
const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person)
Este é um padrão muito comum. Em vez de atualizar a propriedade com um novo valor, realmente queremos alterar o valor aplicando a função ao valor antigo, como fizemos aqui.
Não conheço uma boa maneira de escrever isso com menos duplicação e estilo menos rigoroso, com as ferramentas que aprendemos anteriormente.
Ramda mais uma vez nos salva com a função evoluir . evolve
aceita um objeto e permite especificar funções de transformação para as propriedades que queremos alterar. Vamos refratar o celebrateBirthday
ao usar evolve
:
const celebrateBirthday = evolve({ age: inc })
Este código diz que converteremos o objeto especificado (que não é exibido devido ao estilo de força bruta) criando um novo objeto com as mesmas propriedades e valores, mas a propriedade age
será obtida aplicando inc
ao valor original da propriedade age
.
evolve
pode transformar muitas propriedades ao mesmo tempo e até em vários níveis de aninhamento. A transformação do objeto pode ter a mesma imagem que o objeto mutável terá e evolve
passará recursivamente entre as estruturas, usando as funções de transformação na forma especificada.
Observe que evolve
não adiciona novas propriedades; se você especificar uma transformação para uma propriedade que não ocorra no objeto que está sendo processado, o evolve
simplesmente a ignorará.
Eu descobri que evolve
rapidamente se tornando um cavalo de batalha em meus aplicativos.
Mesclar objetos
Às vezes, você precisa combinar dois objetos. Um caso típico é quando você tem uma função que aceita opções nomeadas e deseja combiná-las com as opções padrão. O Ramda fornece uma função de mesclagem para essa finalidade.
function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) }
merge
retorna um novo objeto contendo todas as propriedades e valores de ambos os objetos. Se ambos os objetos tiverem a mesma propriedade, o valor do segundo argumento será obtido.
A presença dessa regra com um segundo argumento vencedor torna significativo o uso da merge
como uma ferramenta independente, mas menos significativa em situações de transporte. Nesse caso, você geralmente precisa preparar uma série de transformações para um objeto, e uma dessas transformações é a união de alguns novos valores de propriedade. Nesse caso, você desejará que o primeiro argumento vença em vez do segundo.
Tentar usar apenas merge(newValues)
no pipeline não dará o que gostaríamos de obter.
Para essa situação, geralmente crio meu próprio utilitário chamado reverseMerge
. Pode ser escrito como const reverseMerge = flip(merge)
. A chamada inversa troca os dois primeiros argumentos da função que se aplica a ela.
merge
executa uma mesclagem de superfície. Se os objetos, quando combinados, tiverem uma propriedade cujo valor é um subobjeto, esses subobjetos não serão mesclados. No momento, o Ramda não possui uma capacidade de mesclagem profunda (O artigo original que estou traduzindo já possui informações desatualizadas sobre esse tópico. Hoje, o Ramda possui funções como mergeDeepLeft , mergeDeepRight para mesclar objetos recursivamente profundos e outros métodos para mesclar ).
Observe que a merge
aceita apenas dois argumentos. Se você deseja combinar muitos objetos em um, pode usar mergeAll , que exige uma combinação de objetos.
Conclusão
Hoje, temos um conjunto maravilhoso de ferramentas para trabalhar com objetos em um estilo declarativo e imutável. Agora podemos ler, adicionar, atualizar, excluir e transformar propriedades em objetos sem alterar os objetos originais. E podemos fazer todas essas coisas em um estilo que facilita a combinação de funções entre si.
Seguinte
Agora podemos trabalhar com objetos em um estilo imutável, mas e as matrizes? "Imunidade e matrizes" nos dirão o que fazer com eles.