Ramda Thinking: Persistence and Objects

1. First steps
2. We combine functions
3. Partial application (currying)
4. Declarative programming
5. Ruleless Notation
6. Immutability and objects
7. Immutability and arrays
8. Lenses
9. Conclusion


This post is the sixth part of a series of articles on functional programming called "Thinking in the Ramda Style".


In the fifth part, we talked about writing functions in the no-point notation style, where the main argument with the data for our function is not explicitly indicated.


At that time, we could not rewrite all our functions in a pointless style, because we did not have some of the tools needed for this. It is time to study them.


Reading object properties


Let's look again at the example of the definition of people who have the right to vote, which we considered in the fifth part :


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) 

As you can see, we made isCitizen and isEligibleToVote non- isEligibleToVote , but we cannot do this with the first three functions.


As we learned in the fourth part , we can make our functions more declarative through the use of equals and gte . Let's start with this:


 const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18) 

To make these functions pointless, we need a way to construct a function so that we can use the person variable at the end of an expression. The problem is that we need to access the person properties, now we know the only way how this can be done - and it is imperative.


prop


Fortunately, Ramda once again comes to our aid. It provides a prop function for accessing properties of objects.


Using prop , we can rewrite person.birthCountry to prop('birthCountry', person) . Let's do that:


 const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

Wow, now it looks like something much worse. But let's continue our refactoring. Let's change the order of the arguments that we pass to equals so that prop comes last. equals works in the reverse order, so that we do not break anything:


 const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18) 

Next, let's use the currying, the natural property equals and gte , in order to create new functions to which the result of the prop call will be applied:


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person)) 

It still looks worse, but let's continue. Let's apply the advantage of currying again for all the prop calls:


 const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person)) 

Again, somehow not very. But now we see a familiar pattern. All our functions have the same image f(g(person)) , and as we know from the second part , this is equivalent to compose(f, g)(person) .


Let's apply this advantage to our code:


 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) 

Now we got something. All our functions look like person => f(person) . And we already know from the fifth part that we can make these functions pointless.


 const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age')) 

When we started, it was not obvious that our methods did two things. They addressed the property of the object and prepared some operations with its value. This pointless-style refactoring made this very obvious.


Let's take a look at some of the other tools that Ramda provides for working with objects.


pick


Where prop reads one property of an object and returns its value, pick reads many properties from an object and returns a new object only with them.


For example, if we need only the names and years of the people, we can use pick(['name','age'], person) .


has


If we just want to know that our object has a property, without reading its value, we can use the has function to check its properties and also hasIn to check the prototype chain: has('name', person) .


path


Where prop property of an object, path goes deeper into nested objects. For example, we want to pull out the zip code from a deeper structure: path(['address','zipCode'], person) .


Note that the path more forgiving than prop . path will return undefined if anything on the path (including the original argument) is null or undefined , while prop will cause an error in such situations.


propOr / pathOr


propOr and pathOr are similar to prop and path combined with defaultTo . They provide you with the ability to specify a default value for a property or path that will not be found in the object being studied.


For example, we can provide a placeholder when we do not know the name of the person: propOr('<Unnamed>, 'name', person) . Note that unlike prop , propOr will not cause an error if the person is null or undefined ; instead, it will return the default value.


keys / values


keys returns an array containing all the names of all the known properties of the object. values will return the values ​​of these properties. These functions can be useful when combined with the functions of iteration through collections, which we learned about in the first part .


Adding, updating and deleting properties


Now we have a lot of tools to read from objects in a declarative style, but what about making changes?


Since immutability is important to us, we do not want to change objects directly. Instead, we want to return new objects that have changed in the way we desire.


Again, the Ramda provides us with many utilities.


assoc / assocPath


When we program in an imperative style, we can set or change the name of a person through an assignment operator: person.name = 'New name' .


In our functional, immutable world, we can instead use assoc : const updatedPerson = assoc('name', 'newName', person) .


assoc returns a new object with the property value added or updated, leaving the original object unchanged.


We also have an assocPath to update the attached property: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person) .


dissoc / dissocPath / omit


What about removing properties? Imperatively, we may want to say delete person.age . In Ramda, we will use dissoc : `const updatedPerson = dissoc ('age', person)


dissocPath is about the same, but works on deeper object structures: dissocPath(['address', 'zipCode'], person) .


We also have omit , which can remove several properties at once: const updatedPerson = omit(['age', 'birthCountry'], person) .


Please note that pick and omit bit similar and complement each other very nicely. They are very convenient for whitelists (save only a certain set of properties using pick ) and blacklists (get rid of certain properties using omit ).


Object Transformation


Now we know enough to work with objects in declarative and immutable style. Let's write the function celebrateBirthday , which updates the age of the person on her birthday.


 const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person) 

This is a very common pattern. Instead of updating the property to a new value, we actually want to change the value through applying the function to the old value, as we did here.


I don’t know a good way to write this with less duplication and in the pointless style, having those tools that we learned about earlier.


Ramda once again saves us with the evolve function. evolve accepts an object and allows you to specify transformation functions for the properties that we want to change. Let's refactor celebrateBirthday to use evolve :


 const celebrateBirthday = evolve({ age: inc }) 

This code says that we will transform the specified object (which is not displayed due to the pointless style) through creating a new object with the same properties and values, but the age property will be obtained by applying inc to the original value of the age property.


evolve can transform multiple properties at once and even at multiple levels of nesting. The transformation of an object can have the same image as the object being modified, and the evolve will recursively pass between the structures using the transformation functions in the specified form.


Note that evolve does not add new properties; if you specify a transformation for a property that is not found in the object being processed, evolve simply ignore it.


I found that evolve quickly becoming the workhorse in my applications.


Merge objects


Sometimes you need to combine two objects together. A typical case is when you have a function that takes named options and you want to combine them with the default options. Ramda provides a merge function for this purpose.


 function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) } 

merge returns a new object containing all the properties and values ​​from both objects. If both objects have the same property, then the value of the second argument will be obtained.


Having this rule with a winning second argument makes it worthwhile to use merge as a self-sufficient tool, but less meaningful in situations with conveyors. In this case, you often need to prepare a series of transformations for an object, and one of these transformations is the combination of some new property values. In this case, you want the first argument to win instead of the second.


Attempting to simply use merge(newValues) in a pipeline will not give what we would like to receive.


For this situation, I usually create my own utility called reverseMerge . It can be written as const reverseMerge = flip(merge) . A call to flip swaps the first two arguments of the function that applies to it.


merge performs a surface merge. If the objects, when combined, have a property whose value is a subobject, then these subobjects do not merge. Ramda does not currently have the ability to "deep merge" (The original article, the translation of which I am doing, already has outdated information on this topic. Today Ramda has functions such as mergeDeepLeft , mergeDeepRight for recursive deep merging of objects, as well as other methods for merging ).


Note that merge takes only two arguments. If you want to combine multiple objects into one, you can use mergeAll , which accepts an array of objects to combine.


Conclusion


Today we received a wonderful set of tools for working with objects in a declarative and immutable style. Now we can read, add, update, delete and transform properties in objects without changing the original objects. And we can do all these things in a style that makes it easy to combine functions with each other.


Further


Now we can work with objects in an immutable style, but what about arrays? Immunity and Arrays will tell us what to do with them.

Source: https://habr.com/ru/post/414337/


All Articles