Я разрабатываю приложение с графическим интерфейсом, интенсивно работаю с графикой - для примера вы можете думать об этом как о векторном редакторе. Очень заманчиво сделать все структуры данных неизменяемыми, чтобы я мог без особых усилий отменить / повторить, скопировать / вставить и многое другое.
Для простоты я буду использовать следующий пример - приложение используется для редактирования многоугольников, поэтому у меня есть объект «Многоугольник», который представляет собой просто список неизменяемых точек:
Scene -> Polygon -> Point
И поэтому в моей программе есть только одна изменяемая переменная - та, которая содержит текущий объект Scene. У меня возникает проблема, когда я пытаюсь реализовать перетаскивание точек - в изменяемой версии я просто беру Point
объект и начинаю изменять его координаты. В неизменном варианте - я застрял. Я мог бы хранить индексы Polygon
в текущем Scene
, индекс перетаскиваемой точки Polygon
и заменять его каждый раз. Но этот подход не масштабируется - когда уровень композиции становится равным 5 и более, шаблон становится невыносимым.
Я уверен, что эта проблема может быть решена - в конце концов, есть Haskell с полностью неизменными структурами и монадой ввода-вывода. Но я просто не могу найти как.
Можете ли вы дать мне подсказку?
Ответы:
Вы абсолютно правы, этот подход не масштабируется, если вы не можете обойти шаблон . В частности, изменен шаблон для создания совершенно новой сцены с крошечной частью. Однако многие функциональные языки предоставляют конструкцию для работы с такого рода манипуляциями с вложенными структурами: линзы.
Линза - это в основном метод получения и установки неизменных данных. Объектив имеет фокус на какую-то небольшую часть большей структуры. Имея линзу, вы можете сделать с ней две вещи: вы можете просмотреть небольшую часть значения более крупной структуры или установить небольшую часть значения более крупной структуры на новое значение. Например, предположим, что у вас есть объектив, который фокусируется на третьем элементе в списке:
Этот тип означает, что большая структура - это список вещей, и небольшая часть является одной из таких вещей. Учитывая этот объектив, вы можете просмотреть и установить третий элемент в списке:
Линзы полезны потому, что они представляют собой значения, представляющие методы получения и установки, и вы можете абстрагироваться над ними так же, как и другие значения. Вы можете создавать функции, которые возвращают линзы, например,
listItemLens
функцию, которая принимает числоn
и возвращает линзу, просматривая этотn
элемент в списке. Кроме того, линзы могут быть составлены :Каждый объектив инкапсулирует поведение для обхода одного уровня структуры данных. Комбинируя их, вы можете устранить шаблон для преодоления нескольких уровней сложных структур. Например, предположим, что у вас есть объект,
scenePolygonLens i
который просматривает 3-i
й полигон в сцене, и объект,polygonPointLens n
который просматриваетnth
точку в полигоне, вы можете создать конструктор линз для фокусировки только на конкретной точке, которая вас интересует во всей сцене, например:Теперь предположим, что пользователь щелкает точку 3 многоугольника 14 и перемещает ее на 10 пикселей вправо. Вы можете обновить свою сцену так:
Это хорошо содержит весь шаблон для обхода и обновления Сцены внутри
lens
, все, о чем вам нужно заботиться, это то, на что вы хотите изменить точку. Вы можете дополнительно абстрагировать это с помощьюlensTransform
функции, которая принимает линзу, цель и функцию для обновления вида цели через линзу:Это берет функцию и превращает ее в «средство обновления» для сложной структуры данных, применяя функцию только к представлению и используя ее для создания нового представления. Итак, возвращаясь к сценарию перемещения 3-й точки 14-го многоугольника вправо на 10 пикселей, это можно выразить
lensTransform
примерно так:И это все, что вам нужно, чтобы обновить всю сцену. Это очень мощная идея, и она очень хорошо работает, когда у вас есть несколько полезных функций для создания линз, которые просматривают фрагменты ваших данных, которые вам небезразличны.
Однако в настоящее время все это довольно просто, даже в сообществе функционального программирования. Трудно найти хорошую библиотечную поддержку для работы с линзами, и еще сложнее объяснить, как они работают и каковы преимущества для ваших коллег. Возьмите этот подход с зерном соли.
источник
Я работал над точно такой же проблемой (но только с 3 уровнями композиции). Основная идея состоит в том, чтобы клонировать, а затем изменить . В неизменном стиле программирования клонирование и модификация должны происходить вместе, что становится объектом команды .
Обратите внимание, что в непостоянном стиле программирования клонирование было бы необходимо в любом случае:
В непостоянном стиле программирования
В неизменном стиле программирования,
источник
Преимущество глубоко неизменяемых объектов заключается в том, что для глубокого клонирования чего-либо просто необходимо скопировать ссылку. У них есть недостаток, заключающийся в том, что даже небольшое изменение глубоко вложенного объекта требует создания нового экземпляра каждого объекта, в который он вложен. У изменяемых объектов есть преимущество в том, что изменение объекта легко - просто сделайте это - но глубокое клонирование объекта требует создания нового объекта, который содержит глубокий клон каждого вложенного объекта. Хуже того, если кто-то хочет клонировать объект и вносить изменения, клонировать этот объект, вносить другие изменения и т. Д., То, независимо от того, насколько велики или малы изменения, нужно сохранять копию всей иерархии для каждой сохраненной версии состояние объекта. Насти.
Подход, который, возможно, стоит рассмотреть, заключается в определении абстрактного типа «MaybeMutable» с изменяемыми и глубоко неизменяемыми производными типами. Все такие типы будут иметь
AsImmutable
метод; вызов этого метода для глубоко неизменяемого экземпляра объекта просто вернет этот экземпляр. Вызов его для изменяемого экземпляра вернул бы глубоко неизменяемый экземпляр, свойства которого были глубоко неизменяемыми снимками их эквивалентов в оригинале. Неизменяемые типы с изменяемыми эквивалентами будут содержатьAsMutable
метод, который будет создавать изменяемый экземпляр, свойства которого соответствуют свойствам оригинала.Изменение вложенного объекта в глубоко неизменяемом объекте потребовало бы сначала заменить внешний неизменяемый объект на изменяемый, затем заменить свойство, содержащее объект, подлежащий изменению, на изменяемый и т. Д., Но вносить повторяющиеся изменения в один и тот же аспект объекта. общий объект не потребует создания каких-либо дополнительных объектов до тех пор, пока не будет предпринята попытка вызвать
AsImmutable
изменяемый объект (который оставит изменяемые объекты изменяемыми, но вернет неизменные объекты, содержащие те же данные).В качестве простых, но значительных оптимизаций каждый изменяемый объект может содержать кешированную ссылку на объект своего связанного неизменяемого типа, и каждый неизменяемый тип должен кэшировать свое
GetHashCode
значение. При вызовеAsImmutable
изменяемого объекта, прежде чем возвращать новый неизменный объект, убедитесь, что он соответствует кэшированной ссылке. Если это так, верните кэшированную ссылку (оставив новый неизменный объект). В противном случае обновите кэшированную ссылку, чтобы она содержала новый объект и возвращала его. Если это сделано, повторные вызовыAsImmutable
без каких-либо промежуточных мутаций будут давать те же ссылки на объекты. Даже если вы не сэкономите затраты на создание новых экземпляров, вы избежите затрат памяти на их хранение. Кроме того, сравнение на равенство между неизменяемыми объектами может быть значительно ускорено, если в большинстве случаев сравниваемые элементы являются ссылочно-равными или имеют разные хэш-коды.источник