📜 ⬆️ ⬇️

Functional Javascript. We write our lenses, part 1

Hi, Habr.
In this article we will get acquainted with the lenses, find out what they are for, and also implement them in JavaScript.

Why do we need lenses


Let's start, perhaps, with the answer to the question of why we need lenses.

In functional programming, immutable data structures are widely used. Working with them is significantly different compared to the variable data.
')
This is based on the fact that when a part of an immutable data structure changes, a copy of it is created that differs from the original by the same modified part. Full copying of the entire original structure is not efficient, therefore the new structure usually uses references to unchanged parts from the original.

Example:

Suppose we have a data structure:
var user = { name: ' ', address: { city: '', street: '', house: '' } }; 


Our task is to change the meaning of the name.

If we work with this structure as a variable one, then it is enough just to change the value in the user object:

 function setName(value, user) { user.name = value; return user; } 


But if we work with this structure as unchangeable, then we do not have the right to change the data in the original object. We need to create a new user2 object in which to put all the values ​​from user with the exception of the new name.

Full copy option:

 function setName(value, user) { return { name: value, address: { city: user.address.city, street: user.address.street, house: user.address.house } }; } 


PS: The example conveys only the essence. For good, there should be checks that user! = Null, user.adress! = Null.

Partial copy option:

 function setName(value, user) { return { name: value, address: user.address }; } 


I think the scheme is clear, so we will write general functions for working with the properties of the structure:

 //  function get(prop) { return function(item) { return item[prop]; }; } //    function setMutable(prop) { return function(value, item) { item[prop] = value; return item; } } //     function setImmutable(prop) { return function(value, item) { var props = properties(item), //     copy = props.reduce(function(lst, next) { lst[next] = item[next]; return lst; }, {}); copy[prop] = value; //    return copy; }; } //    obj function properties(obj) { var key, lst = []; for (key in obj) { if (obj.hasOwnProperty(key)) { lst.push(key); } } return lst; } 


Now we can use these functions to generate heters and setters.

The original example can be rewritten as:

 setName = setMutable('name') //   setName = setImmutable('name') //  getName = get('name') //      


Now suppose that we need to change the value of city from user.

Let's set the task more generally and write the heters and seters allowing to work with the city through the user object.

For a changeable structure, the implementation might look like

 function getUserCity(user) { return user.address.city; } function setUserCity(value, user) { user.address.city = value; return user; } 


Or the same, but in a more functional style, using the already defined functions get, setMutable:

 var getAddress = get('address'), getCity = get('city'), getUserCity = compose(getCity, getAddress), //  compose       (  )          . ..    getUserCity = function(item) { return getCity(getAddress(item)); }, setCity = setMutable('city'), setUserCity = function (value, item) { setCity(value, getAddress(item)) return item; } var newUser = setUserCity(' city', user); getUserCity(newUser) == ' city' // true //PS function compose(func1, func2) { return function() { return func1(func2.apply(null, arguments)); }; } 


Let's try to implement the same for immutable structure:

 var getAddress = get('address'), getCity = get('city'), getUserCity = compose(getCity, getAddress), setCity = setImmutable('city'), setUserCity = function (value, item) { setCity(value, getAddress(item)) return item; }; var newUser = setUserCity(' city', user); getUserCity(newUser) == ' city' // true 


At first glance, everything is fine. But pay attention to the setUserCity function. In it, we get a new value for the input for the city value and the item user, change the value of the city and ... return the original item object. But this contradicts the definition of immutable data structures. When changing any part of it, we must create a new object.

Using the setUserCity function turns our immutable object back into a mutable. To verify this, let's execute the following code:

 var newUser1 = setUserCity('city1', user), newUser2 = setUserCity('city2', user); newUser1 == newUser2 //true 

To fix this, you need to rewrite the value of the address of the user item , and return the new user
 var setAddress = setImmutable('address'), setUserCity = function (value, item) { var address = setCity(value, getAddress(item)); return setAddress(address, user); }, newUser1 = setUserCity('city1', user), newUser2 = setUserCity('city2', user); newUser1 == newUser2 //false 


Now everything works as it should.

Findings:

The composition of heteros is the same for both variable and non-variable structures, but the construction of setters differs significantly.

To build a grid with depth n of a variable data structure, it suffices to use n - 1 heteros and one grid from the last level.

To obtain a setter of an unchangeable structure of depth n, n - 1 heteros and n sets are necessary, since It is necessary to update all levels starting from 0 (source object).

To simplify the construction (layout) of the seters (and heteros) of immutable data structures, it is convenient to use the lens tool.

Lenses


We found out that the layout of the setters for immutable structures is not a trivial task, since it requires a list of all heteros and sets.

But let's introduce an abstraction called a lens, which is nothing more than a pair of a hetero and a seter:

 var lens = Lens(getter, setter) // . 


We also introduce the trivial get and set operations that will simply duplicate the functionality transferred to the hetero and seter lens:

 function Lens(getter, setter) { return { get: getter, set: setter }; } 


And now let's look once more at the setUserCity function. When diving from level A to level B, we need getters and setters A and B. But we just introduced a new abstraction Lens . Why should not the composition of seters and heteras separately be replaced by the composition of their lenses?

Let's introduce a new operation on compose lenses, which builds a composition of two lenses:

 function Lens(getter, setter) { return { compose: function (lens) { return Lens(get2, set2); function get2(item) { return lens.get(getter(item)); } function set2 (value, item) { var innerValue = lens.set(value, getter(item)); return setter(innerValue, item); } }, get: getter, set: setter }; } 


Let's try to solve our problem with the use of lenses:

 var addressLens = Lens(getAddress, setAddress), //    cityLens = Lens(getCity, setCity), //    addressCityLens = addressLens.compose(cityLens); //    addressCityLens.set(' city', user); //  city  user,   


Pay attention to the composition of the lenses. It is very similar to the composition through the user.address.city point. Adding a new lens as if plunges us to a lower level.

In practice, quite often there will be a need to change the value given its current value, so let's expand our abstraction with another modify operation:

 function Lens(getter, setter) { return { modify: function (func, item) { var value = getter(item); return setter(func(value), item); }, compose: function (lens) { return Lens(get2, set2); function get2(item) { return lens.get(getter(item)); } function set2 (value, item) { var innerValue = lens.set(value, getter(item)); return setter(innerValue, item); } }, get: getter, set: setter }; } 


Syntactic sugar


We learned what lenses are, what they are for and how to use them. But let's think about how to make working with lenses easier.
Firstly, it is unlikely that we will need to create a lens with a hetero and a setter for different fields (although this can theoretically be done). So let's overload the lens constructor. It will take the name of the property and automatically generate a lens for it.

 function Lens(getter, setter) { //  1 ,     if (arguments.length == 1) { var property = arguments[0]; getter = get(property); setter = setImmutable(property); } return { modify: function (func, item) { var value = getter(item); return setter(func(value), item); }, compose: function (lens) { return Lens(get2, set2); function get2(item) { return lens.get(getter(item)); } function set2 (value, item) { var innerValue = lens.set(value, getter(item)); return setter(innerValue, item); } }, get: getter, set: setter }; } 


Now the original example can be written as:

 Lens('address').compose(Lens('city')).set(' city', user); 


Creating a lens easier, but the composition looks rather cumbersome. Let's write a small interpreter that will create lenses and build their composition. At the entrance it will take a list of the names of the properties for which you want to create lenses. And the composition operation will be set by a point (in the best traditions of Haskell, but unlike it, it will be carried out from left to right).
As a result, our example should be transformed into something like this:

 lens('address.city').set(' city', user); 


Well, pretty close to user.address.city . Let's implement the lens function

 function lens(cmd) { var lenses = cmd.split('.') .map(pass1(Lens)); return lenses.reduce(function(lst, next) { return lst.compose(next); }); } //         , //     ,   function pass1(func) { return function(x) { return func(x); }; } 


Pay attention to the pass1 function. The fact is that the map transmits more than 1 parameter to callback, so if we write map(Lense) , the version of Lense accepts the heteroter and the seter as input will be used. Therefore, we wrap our pass1 function, which ensures that only the first parameter passed to it enters the Lense .

In the second part, we will look at how to make friends with the lenses and the Nothing monad.

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


All Articles