Functional Javascript. Writing Your Lenses Part 1

Hi, Habr.
In this article, we will get to know lenses, find out why they are needed, and also implement them in JavaScript.

Why are lenses needed?


Let's start with the answer to the question why lenses are needed.

Functional programming makes extensive use of immutable data structures. Working with them is significantly different compared to mutable data.

This is based on the fact that when a part of an immutable data structure is changed, a copy of it is created that differs from the original by this changed part. Full copying of the entire original structure is not effective, so 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 mutable, then just change the value in the user object:

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


But if we work with this structure as immutable, 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 except for 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 understandable, so we will write general functions for working with structure properties:

//генерация гетеров
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 getters and seters.

The original example can be rewritten like this:

setName = setMutable('name') //Для изменяемой структуры
setName = setImmutable('name') //Для неизменяемой
getName = get('name') //Для получения имени в обоих случаях


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

We set the task more generally and write getters and seters that allow you to work with city through the user object.

For a variable structure, an implementation might look like this:

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


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

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
//P.S.
function compose(func1, func2) {
  return function() {
    return func1(func2.apply(null, arguments));
  };
}


Let's try to implement the same for an 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 function setUserCity. In it, we get a new value for city valueand the user item, change the value of city and ... return the original object item. But this contradicts the definition of immutable data structures. When changing any part of it, we must create a new object.

Using a function setUserCity turns our immutable object back into 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 address value 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.

Conclusions:

The composition of getters is carried out identically for both mutable and immutable structures, but the construction of seters differs significantly.

To build a seter with depth n of a variable data structure, it is sufficient to use n - 1 getters and one seter from the last level.

To obtain a seter of an invariable structure of depth n, n - 1 getters and n setters are necessary, because it is necessary to update all levels starting from 0 (source object).

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

Lenses


We found that the layout of seters for immutable structures is not a trivial task, since it requires a list of all getters and seters.

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

var lens = Lens(getter, setter) //конструктор линзы.


We also introduce the trivial operations get, set, which will simply duplicate the functionality of the getter and setter transmitted to the lens:

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


Now let's take another look at the function setUserCity. When diving from level A to level B, we need geters and seters A and B. But we just introduced a new abstraction Lens. Why not replace the composition of seters and getters individually with the composition of their lenses?

Let's introduce a new lens operation composethat 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 using 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 dotted composition user.address.city. Adding a new lens immerses us one level below.

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

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, why they are needed 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 getter and a setter on 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);


The creation of lenses has been facilitated, but the composition looks rather bulky. Let's write a small interpreter that will create lenses and build their composition. At the entrance, he will take a list of property names for which you need 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 transform into something like this:

lens('address.city').set('новый city', user);


Well, pretty close to user.address.city. Let's implement a functionlens

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 function pass1. The fact is that it maptransfers more than 1 parameter to callbaсk, so if we write, map(Lense)then the version Lensereceiving the getter and setter will be used . Therefore, we wrap our function pass1, which ensures that Lenseonly the first parameter passed to it gets into it.

In the second part, we will look at how you can make friends the lens and the Nothing monad.

Also popular now: