Как стать автором
Обновить

Комментарии 42

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

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

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

  var sergei = { name: "Сергей" }'
  sergei.__proto__ = user;



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

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

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

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

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

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

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

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

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

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

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

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

Это не иммутабельность, если оригинальный user доступен. Также будут неожиданные последствия при модификации вложенных свойств.
Вместо __proto__ следует использовать Object.create.
А вообще, есть подобная идея, но для внутренних структур данных ЯП, которые будут делать экономное клонирование.
В 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


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


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

Функциональное программирование в веб-проектах используется постоянно.
$('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, тоже берет начало в функциональных языках.
Я то ожидал больше примера про линзы.
>Это уже функциональное программирование.

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

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

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

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

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

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

Стоп. В статье ни слова нет о том, для чего нужны линзы (не какую функцию выполняют, а именно практическая ценность). Хотелось бы пару абзацев с примерами. Пока ума не приложу как их использовать в проектах на JS
Линзы нужны, чтобы добавить проекту, написанному в функциональной архитектуре, модульности. До тех пор, пока проект написан императивно (на тех же jquery, knockout или angular) — линзы никому не нужны.
Не могли бы Вы раскрыть мысль? В каком смысле «добавить модульности»?
Допустим, на странице есть два независимых блока. У каждого из них есть свое состояние, но нам надо сформировать состояние всей страницы.
Тогда мы пишем (условно):
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);
}
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории