Функциональный Javascript. Пишем свои линзы, часть 1

Привет, хабр.
В данной статье мы познакомимся с линзами, узнаем для чего они нужны, а также реализуем их на JavaScript.

Зачем нужны линзы


Начнем, пожалуй, с ответа на вопрос, зачем же нужны линзы.

В функциональном программировании широко используются неизменяемые структуры данных. Работа с ними значительно отличается по сравнению с изменяемыми данными.

В основе этого лежит тот факт, что при изменении какой-либо части неизменяемой структуры данных создается ее копия, отличающаяся от оригинала этой самой измененной частью. Полное копирование всей исходной структуры не эффективно, поэтому новая структура как правило использует ссылки на неизмененные части из оригинала.

Пример:

Пусть у нас есть структура данных:
var user = {
	name: 'Имя пользователя',
	address: {
		city: 'Город',
		street: 'Улица',
		house: 'Дом'
	}
};


У нас стоит задача поменять значение имени.

Если мы работаем с этой структурой как с изменяемой, то достаточно просто изменить значение в объекте user:

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


Но если мы работаем с этой структурой как с неизменяемой, то мы не имеем право менять данные в исходном объекте. Нам нужно создать новый объект user2 в который поместить все значения из user за исключением нового name.

Вариант с полным копированием:

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


PS: Пример передает только суть. По хорошему тут должны быть проверки на то, что user != null, user.adress != null.

Вариант с частичным копированием:

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


Думаю схема понятна, поэтому напишем общие функции для работы со свойствами структуры:

//генерация гетеров
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;
}


Теперь мы можем использовать эти функции для генерации гетеров и сетеров.

Исходный пример может быть переписан так:

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


Теперь предположим, что нам нужно поменять значение city у user.

Поставим задачу более обще и напишем гетеры и сетеры позволяющие работать с city через объект user.

Для изменямой структуры реализация может выглядеть так:

function getUserCity(user) {
	return user.address.city;
}

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


Или тоже самое, но в более функциональном стиле, используя уже определенные функции 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

//P.S.
function compose(func1, func2) {
  return function() {
    return func1(func2.apply(null, arguments));
  };
}


Давайте попробуем реализовать тоже для неизменямой структуры:

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


На первый взгляд все нормально. Но обратите внимание на функцию setUserCity. В ней мы получаем на вход новое значение для city value и пользователя item, изменяем значение city и… возвращаем исходный объект item. Но это противоречит определению неизменямых структур данных. При изменении любой его части мы должны создать новый объект.

Применение функции setUserCity превращает наш неизменяемый объект обратно в изменяемый. Чтобы это проверить давайте выполним следующий код:

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

Чтобы исправить это, нужно переписать значение address у пользователя item, и вернуть нового пользователя
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 


Теперь все работает как надо.

Выводы:

Композиция гетеров осуществляется одинаково как для изменяемых так и для неизменяемых структур, но построение сетеров отличается значительно.

Для построения сетера глубиной n изменяемой структуры данных достаточно использования n — 1 гетеров и одного сетера с последнего уровня.

Для получения сетера неизменяемой структуры глубиной n необходимо n — 1 гетеров и n сетеров, т.к. необходимо обновлять все уровни начиная с 0 (исходный объект).

Для упрощения построения (компоновки) сетеров (и гетеров) неизменяемых структур данных удобно использовать инструмент линзы.

Линзы


Мы выяснили, что компоновка сетеров для неизменяемых структур является не тривиальной задачей, так как требует наличие списка всех гетеров и сетеров.

Но давайте введем абстракцию называемую линзой, которая есть не что иное как пара гетера и сетера:

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


Так же введем тривиальные операции get, set которые будут просто дублировать функционал переданных линзе гетера и сетера:

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


А теперь давайте еще разок глянем на функцию setUserCity. При погружении с уровня A на уровень B нам нужны гетеры и сетеры A и B. Но мы ведь только что ввели новую абстракцию Lens. Почему бы композицию сетеров и гетеров по отдельности не заменить композицией их линз?

Давайте введем новую операцию на линзах compose, которая строит композицию двух линз:

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
    };
}


Давайте попробуем решить нашу задачу с использованием линз:

var addressLens = Lens(getAddress, setAddress), //строим линзу для адреса
    cityLens = Lens(getCity, setCity), //строим линзу для города
    addressCityLens = addressLens.compose(cityLens); //компонуем две линзы вместе

    addressCityLens.set('новый city', user); //изменяем значение city через user, используя линзу


Обратите внимание на композицию линз. Она очень похожа на композицию через точку user.address.city. Добавление новой линзы как бы погружает нас на уровень ниже.

На практике довольно часто будет встречаться потребность к изменению значения с учетом его текущего значения, поэтому давайте расширим нашу абстракцию еще одной операцией 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
    };
}


Синтаксический сахар


Мы узнали что такое линзы, для чего они нужны и как ими пользоваться. Но давайте подумаем, как можно сделать работу с линзами проще.
Во первых, мало вероятно, что нам понадобится создавать линзу с гетером и сетером на разные поля (хотя это теоретически можно сделать). Поэтому давайте перегрузим конструктор линз. Он будет принимать имя свойства и автоматически генерировать для него линзу.

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
    };
}


Теперь исходный пример может быть записан как:

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


Создание линз облегчилось, но вот композиция выглядит довольно громоздко. Давайте напишем небольшой интерпретатор, который будет создавать линзы и строить их композицию. На вход он будет принимать список из названий свойств для которых нужно создать линзы. А операция композиции будет задаваться точкой (в лучших традициях Haskell, но в отличие от него осуществляться она будет слева направо).
Врезультате наш пример должен преобразоваться в нечто такое:

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


Что же, довольно близко к user.address.city. Давайте реализуем функцию lens

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);
	};
}


Обратите внимание на функцию pass1. Дело в том, что map передает в callbaсk больше чем 1 параметр, поэтому если мы напишем map(Lense) то будет использоваться версия Lense принимающая на вход гетер и сетер. Поэтому мы оборачиваем нашу функцию pass1, которая гарантирует, что в Lense попадет только первый переданный ей параметр.

Во второй части мы рассмотрим, как можно подружить линзы и монаду Nothing.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 42

    +17
    Не знаю, зачем нужны линзы в Javascript — но только благодаря этой статье я наконец-то понял, как они работают в Хаскеле…
      +4
      Основной целью статьи было заинтересовать людей функциональным программированием.
      Насчет применения линз в Javascript: я не уверен на 100%, но возможно им найдется место в React.js. Насколько я знаю там нашли применение неизменяемые структуры данных. Могу ошибаться, т.к. знаком с ним только поверхностно.
        0
        Подход замечательный, жаль только, что в JS с иммутабельностью туго.
          0
          Линзы и для mutable вполне в тему.
            0
            Ну это смотря как посмотреть, я бы сказал что с неизменяемостью в js, дела обстоят гораздо лучше чем в c# например. Дуглас Крокфорд на одной из своих презентаций представил интересный подход для достижения неизменяемости, а именно никаких переменных только функции и их параметры. Так же неплохо помогает Object.freeze.
              0
              С тем же Object.freeze мы можем гарантировать что структура будем неизменяемой, но этого недостаточно. Производительность будем ужасная. Тут нужны персистентные структури данных. А это уже на коленке не сделаешь.
                0
                Согласен с неполнотой неизменяемости, но увы этой самой полноты нет ни в F# ни в Scala. А вот насчет производительности Object.freeze ваши сведения неточны. Проблемы с производительностью были на заре внедрения данной фитчи, но потом все пофиксили и сейчас разницы почти не заметно даже в чакре.
                  0
                  Тут дело даже не в производительности Object.freeze. Тут дело в подходе. С Object.freeze на каждое изменение придется копировать всю структуру, и это очень плохо. Никто так не делает. Ниже я писал про персистентные структуры данных, которые применятся при реализации иммутабельних структур, и которые дают очень даже хорошие результаты.
                    0
                    Ну в голом Javascript такого нет. Можно использовать функциональные языки, например ClojureScript. Есть еще Scala.js, но я не знаю насколько она стабильна.
                      0
                      Уже есть — mori, immutable-js. Не настолько удобно как в ClojureScript, но работать можно.
                      0
                      Object.freeze — это просто защита от дурака. Ничто не мешает использовать Object.freeze и персистентные структуры данных совместно.
                        0
                        Никто не мешает, но я не вижу никакого смысла использовать Object.freeze для персистентных структур. Все реализации, которые я видел, используют непрозрачную trie-like структуру, и менять ее вручную — это уже извращение.
                          0
                          Согласен. Тем не менее, я считаю, что ставить знак равенства между Object.freeze и медленной скоростью работы некорректно. Если нам нужен объект всего с 3мя полями — то преобразовывать его в trie-like структуру и будет тем самым извращением.
              0
              Я использую react, и у меня состояние через линзы меняется. Код самописный, подходящих библиотек я пока не видел. Состояние у меня mutable, но линзы и с mutable полезны (хотя это уже не совсем линзы, а больше first-class properties).

              Плюс в том, что стейт един, т.е. не размазан по компонентам. В результате им можно оперировать одним куском — сохранять, загружать, делать undo и revert, отвалидировать и сразу понять валидна ли вся форма целиком, и т.п.
                0
                >Основной целью статьи было заинтересовать людей функциональным программированием.
                У вас получилось. Первое впечатление от ФП я получил в статье Списки из lambda-функций, которая показала красоту ФП. Ваша статья усилила это впечатление. Пора наконец пойти и поставить себе что-то чисто функциональное.
              +6
              Иммутабельные структуры данных — это круто. Однако только в тех языках, где все построено вокруг неизменяемости. Т.е. библиотеки, инструменты, платформы. Имхо.
                0
                А почему не так?

                  var user = {
                      name: 'Имя пользователя',
                      address: {
                          city: 'Город',
                          street: 'Улица',
                          house: 'Дом'
                      }
                  };
                
                  var sergei = { name: "Сергей" }'
                  sergei.__proto__ = user;
                
                


                Почему без прототипов?
                  +1
                  Для одного-двух раз такое решение — идеальное. Но если требуется, скажем, в цикле изменить пару полей 100 раз — то итоговая цепочка из 100 прототипов убьет всю производительность.
                    0
                    Видимо, зависит от того, как применять. Я далек от функционального программирования. Расскажите, а когда такая надобность возникает? Простой пример есть?

                    Я к тому, что иногда создание ссылки на прототип может быть «дешевле», чем копирование всех полей по честному.
                      +1
                      Простого примера нету — просто потому что любая короткая программа куда проще пишется в императивном стиле, нежели в функциональном.

                      По поводу же того, зачем это вообще нужно… Представьте себе такую архитектуру: есть некоторый объект (неизменяемый), называемый состоянием. Есть какое-то число методов, которые принимают как параметр состояние и возвращают его модифицированную копию (переходы между состояниями). Любое изменение состояния возможно только в таком виде:
                      state = transition(state, arg1, arg2, arg3,...);

                      Что дает такая архитектура? Она дает возможность хранить любое состояние неограниченно долго и при необходимости к нему возвращаться.

                      Так, состояние программы можно положить в history, получив тем самым поддержку кнопки back в браузере. Его можно сериализовать в JSON и сохранить в облако. От состояния программы можно даже подсчитать хеш, чтобы затем сравнить его с состоянием программы на другом компьютере!

                      Важно понимать, что в такой архитектуре любое действие пользователя приводит к изменению состояния — а значит, и к его копированию. И если на копирование всех полей состояния требуется каждый раз тратить одинаковое время — то поиск по цепочке прототипов какого-нибудь редкоизменяемого свойства будет происходить все дольше и дольше.
                        0
                        Проблему слишком большого количества диффов можно решить создаваемыми время от времени спепшотами, которые схлопывают все накопленные изменения в один объект без прототипов.
                          0
                          А смысл? Использование прототипов в такой задаче — это оптимизация операции записи, в ущерб чтению. Но какой вообще смысл оптимизировать запись в ущерб чтению, если операций чтения всегда будет больше?
                        +1
                        Видимо, зависит от того, как применять. Я далек от функционального программирования. Расскажите, а когда такая надобность возникает?

                        Вся прелесть использования иммутабельных структур данных заключается в отсутствии побочных эффектов. Это чертовски здорово помогает рассуждать о коде, как он работает, ведь можно исключить целый пласт проблем связанных с тем, что кто-то, где-то поменял структуру твоих данных. Иммутабельние структуры дают тебе гарантию, что:
                        1) данные, которые ты используеш, не будут меняться извне
                        2) ты можеш делать что угодно с данными, и ты не повлияешь на другой код

                        Поэтому, если ты уже взялся использовать иммутабельные данные, будь добр не меняй эти данные in-place, иначе вся суть их использования будет утрачена.

                        Очень хорошо если язык предоставляет такие структуры. Этим он гарантирует отсутствие побочных эффектов для кода, который использует только такой тип данных.

                        Поэтому пример с изменением в цикле — это нормально, ведь тебе нужно соблюдать контракт, ты должен использовать только иммутабельные операции (строго говоря, есть еще transient data structure, с помощью которых можна обойти это ограничений не нарушая иммутальность).

                        Я к тому, что иногда создание ссылки на прототип может быть «дешевле», чем копирование всех полей по честному.

                        Не обязательно копировать всю структуру целиком. Есть целое семейство структур данных, которые позволяють значительно повысить производительность при работе с иммутабельными данными, так называемые persistent data structure.

                      0
                      Это не иммутабельность, если оригинальный user доступен. Также будут неожиданные последствия при модификации вложенных свойств.
                      Вместо __proto__ следует использовать Object.create.
                      А вообще, есть подобная идея, но для внутренних структур данных ЯП, которые будут делать экономное клонирование.
                        +2
                        В javascript не получится сделать жесткой иммутабельности. Все равно будет возможность что-то поменять. Так что это вопрос конвенции. По иммутабельной конвенции мы договорились, что никакие объекты менять нельзя, поэтому мой вариант корректен.

                        Можно вот так оформить:

                           var a = { name: "Sergei", age: 29 };
                           
                           var extend = function(obj, attrs) {
                             var result = new Object();
                           
                             for(i in attrs) {
                               result[i] = attrs[i];
                             }
                           
                            result.__proto__ = obj;
                          
                            return result;
                          }
                          
                          
                          b = extend(a, {city: "Pushkino"});
                        


                        Вместо __proto__ следует использовать Object.create


                        Поясните, пожалуйста. Я не понял.
                          +2
                          мне кажется, если запись происходит чаще, чем чтение, то такой подход будет быстрее. Но если к объекту применить сотню записей и миллион чтений, то подход из статьи будет быстрее.
                            +1
                            В javascript не получится сделать жесткой иммутабельности. Все равно будет возможность что-то поменять. Так что это вопрос конвенции.
                            Насчёт конвенции вы очень верно подметили. В JS очень много вещей отданы на откуп конвенции. Но всё-таки жёсткую иммутабельность для plain-object можно сделать путём глубокого клонирования. Тем не менее, это выходит дороговато, поэтому соглашения тут выигрывают.

                            По поводу __proto__: MDN __proto__.
                            Для создания объекта с заданным прототипом нужно использовать Object.create:

                            // строку
                            var result = new Object();
                            // заменяем на
                            var result = Object.create(obj);
                            // строку
                            // result.__proto__ = obj;
                            // убираем
                            

                            Object.create просто создаёт объект с заданным прототипом. А __proto__ это internal свойство, поэтому его использование некорректно.
                              +1
                              На самом деле есть немного Object.freeze
                                0
                                Object.freeze и Object.seal как бы есть, но они сами являются изменяющими, т.е. они работают на объекте, а не создают иммутабельную копию. Поэтому всё равно нужно предварительно склонировать объект, а раз уж мы его склонировали, то его изменение нам не страшно и Object.freeze как бы опционален (по сути, он ничем не превосходит возможности конвенции).
                                  0
                                  Замораживать можем при создании. Значит, путь жесткой иммутабельности, все-таки, есть.
                                    0
                                    Это допустительная заморозка (да и не глубокая), при попытке изменить свойство у замороженного объекта ничего не произойдёт. Всмысле, вообще ничего, ни эксепшена, ничего. Эффекта можно достичь только в strict-mode.
                                    В обычном же режиме, смысла от такой иммутабельности чуть более, чем нет, потому как мы уже склонировали объект.
                                0
                                А __proto__ это internal свойство, поэтому его использование некорректно.


                                Согласен. Я привел просто пример, которые намекает на путь. Спасибо, за Object.create
                          +1
                          А какие примеры использования функционального программирования и в частности линзы в веб-проектах?
                            –2
                            Отвечу про общий случай использования функционального программирования.

                            Функциональное программирование в веб-проектах используется постоянно.
                            $('a').click(function () {
                            console.log('hello');
                            })

                            function () {
                            console.log('hello');
                            }
                            Это уже функциональное программирование. Т.к. в качестве аргумента мы передаем функцию и можем возвратить функцию.

                            Функции underscore.
                            _.map, _.filter, _.reduce — тоже пришли из функциональных языков.

                            Многие фичи в ES6 берут свое начало из функциональных языков:
                            github.com/lukehoban/es6features#destructuring — pattern matching.
                            github.com/lukehoban/es6features#comprehensions

                            FRP на базе которого был построен в том числе React, тоже берет начало в функциональных языках.
                              +2
                              Я то ожидал больше примера про линзы.
                                +4
                                >Это уже функциональное программирование.

                                Осмелюсь не согласиться. Функцональное программирование, это не все, где передаются функции, потому что само понятие «функция» в функциональных языках и не функциональных различается. Не надо путать анонимные функции и лямбда-функции. Функциональное программирование подразумевает «чистые» функции и отсуствие побочных эффектов, так что данный пример не подходит.

                                >_.map, _.filter, _.reduce — тоже пришли из функциональных языков.

                                А вот это подходит.
                                  0
                                  > Функциональное программирование подразумевает «чистые» функции и отсуствие побочных эффектов, так что данный пример не подходит.

                                  Неа. Функциональное программирование подразумевает функции, как математические объекты. То есть, как бинарные отношения с определёнными свойствами. И функциональное программирование — это оперирование некоторыми вариантами реализаций таких объектов. И ничего не мешает при помощи таких функций выразить не «чистые» процедуры с состояниями и эффектами. Даже можно goto определить.

                                  Другое дело… Что это, эмс, мягко говоря, чесание левой пяткой правого уха. Потому что, всё же, программисты имеют дело не с функциями, а с процессами. Мы, конечно, можем смоделировать процесс через набор функций, но это будет только лишь модель, в которой не будет важных свойств процессов. Которые, например, могут открыто взаимодействовать.

                                  И вообще, всегда возникает вопрос: вот почему lambda-исчисление так активно пропагандируется, а вот о pi-исчислении или о CSP Хоара как-то общественность постоянно забывает. Между тем, это более адекватные реальной жизни формальные модели вычислений.
                              0
                              Мы узнали что такое линзы, для чего они нужны и как ими пользоваться.

                              Стоп. В статье ни слова нет о том, для чего нужны линзы (не какую функцию выполняют, а именно практическая ценность). Хотелось бы пару абзацев с примерами. Пока ума не приложу как их использовать в проектах на JS
                                0
                                Линзы нужны, чтобы добавить проекту, написанному в функциональной архитектуре, модульности. До тех пор, пока проект написан императивно (на тех же jquery, knockout или angular) — линзы никому не нужны.
                                  0
                                  Не могли бы Вы раскрыть мысль? В каком смысле «добавить модульности»?
                                    +1
                                    Допустим, на странице есть два независимых блока. У каждого из них есть свое состояние, но нам надо сформировать состояние всей страницы.
                                    Тогда мы пишем (условно):
                                    var state = {
                                      moduleA: stateA,
                                      moduleB: stateB,
                                    };
                                    

                                    Теперь нам надо научить каждый модуль работать не со своим состоянием, а с глобальным (напоминаю: состояние — вещь неизменяемая в функциональной архитектуре).
                                    Один из способов — это захардкодить в модулях абсолютные пути. Если раньше у нас было
                                    (допустим, состояние первого модуля — это число)
                                    function foo(state) {
                                      state = state+1;
                                      someDiv.textContent = state;
                                      return state;
                                    }
                                    

                                    то теперь нам придется писать как-то так:
                                    function foo(state) {
                                      var count = state.moduleA + 1;
                                      someDiv.textContent = count;
                                      return updateState(state, { moduleA: count });
                                    }
                                    
                                    function updateState(oldstate, newstate) {
                                      for (var k in oldstate)
                                        if (!(k in newstate))
                                          upd[k] = oldstate[k];
                                      return newstate;
                                    }
                                    

                                    Какой недостаток у такого подхода? Все функции модуля A (в данном примере — это фукнкция foo) должны знать, что они находятся именно в модуле A, а не в каком-то другом — иначе они просто не смогут правильно обновить состояние. Модуль нельзя использовать несколько раз…

                                    Что тут можно сделать? Можно операции получения локального состояние модуля из глобального, а также его обновления вынести в отдельные функции — эта пара функций и будет линзой. Код станет таким:
                                    function foo(state, lens) {
                                      var count = lens.get(state)+1;
                                      someDiv.textContent = count;
                                      return lens.set(state, count);
                                    }
                                    
                                0
                                (deleted)

                                Only users with full accounts can post comments. Log in, please.