Неизменные структуры и глубокая иерархия композиции

9

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

Для простоты я буду использовать следующий пример - приложение используется для редактирования многоугольников, поэтому у меня есть объект «Многоугольник», который представляет собой просто список неизменяемых точек:

Scene -> Polygon -> Point

И поэтому в моей программе есть только одна изменяемая переменная - та, которая содержит текущий объект Scene. У меня возникает проблема, когда я пытаюсь реализовать перетаскивание точек - в изменяемой версии я просто беру Pointобъект и начинаю изменять его координаты. В неизменном варианте - я застрял. Я мог бы хранить индексы Polygonв текущем Scene, индекс перетаскиваемой точки Polygonи заменять его каждый раз. Но этот подход не масштабируется - когда уровень композиции становится равным 5 и более, шаблон становится невыносимым.

Я уверен, что эта проблема может быть решена - в конце концов, есть Haskell с полностью неизменными структурами и монадой ввода-вывода. Но я просто не могу найти как.

Можете ли вы дать мне подсказку?

Рогач
источник
@ Job - вот как это работает прямо сейчас, и это доставляет мне много боли. Поэтому я ищу альтернативные подходы - и неизменность кажется идеальной для этой структуры приложения, по крайней мере, до того, как мы добавим к ней взаимодействие с пользователем :)
Rogach
@Rogach: Можете ли вы объяснить больше о своем шаблонном коде?
Rwong

Ответы:

9

Я мог бы хранить индексы полигона в текущей сцене, индекс перетаскиваемой точки в полигоне и заменять его каждый раз. Но этот подход не масштабируется - когда уровень композиции становится равным 5 и более, шаблон становится невыносимым.

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

Линза - это в основном метод получения и установки неизменных данных. Объектив имеет фокус на какую-то небольшую часть большей структуры. Имея линзу, вы можете сделать с ней две вещи: вы можете просмотреть небольшую часть значения более крупной структуры или установить небольшую часть значения более крупной структуры на новое значение. Например, предположим, что у вас есть объектив, который фокусируется на третьем элементе в списке:

thirdItemLens :: Lens [a] a

Этот тип означает, что большая структура - это список вещей, и небольшая часть является одной из таких вещей. Учитывая этот объектив, вы можете просмотреть и установить третий элемент в списке:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

Линзы полезны потому, что они представляют собой значения, представляющие методы получения и установки, и вы можете абстрагироваться над ними так же, как и другие значения. Вы можете создавать функции, которые возвращают линзы, например, listItemLensфункцию, которая принимает число nи возвращает линзу, просматривая этот nэлемент в списке. Кроме того, линзы могут быть составлены :

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

Каждый объектив инкапсулирует поведение для обхода одного уровня структуры данных. Комбинируя их, вы можете устранить шаблон для преодоления нескольких уровней сложных структур. Например, предположим, что у вас есть объект, scenePolygonLens iкоторый просматривает 3- iй полигон в сцене, и объект, polygonPointLens nкоторый просматривает nthточку в полигоне, вы можете создать конструктор линз для фокусировки только на конкретной точке, которая вас интересует во всей сцене, например:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Теперь предположим, что пользователь щелкает точку 3 многоугольника 14 и перемещает ее на 10 пикселей вправо. Вы можете обновить свою сцену так:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

Это хорошо содержит весь шаблон для обхода и обновления Сцены внутри lens, все, о чем вам нужно заботиться, это то, на что вы хотите изменить точку. Вы можете дополнительно абстрагировать это с помощью lensTransformфункции, которая принимает линзу, цель и функцию для обновления вида цели через линзу:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

Это берет функцию и превращает ее в «средство обновления» для сложной структуры данных, применяя функцию только к представлению и используя ее для создания нового представления. Итак, возвращаясь к сценарию перемещения 3-й точки 14-го многоугольника вправо на 10 пикселей, это можно выразить lensTransformпримерно так:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

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

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

Джек
источник
Отличное объяснение! Теперь я понял, что такое линзы!
Винсент Лекрубье
13

Я работал над точно такой же проблемой (но только с 3 уровнями композиции). Основная идея состоит в том, чтобы клонировать, а затем изменить . В неизменном стиле программирования клонирование и модификация должны происходить вместе, что становится объектом команды .

Обратите внимание, что в непостоянном стиле программирования клонирование было бы необходимо в любом случае:

  • Разрешить отменить / повторить
  • Системе отображения может потребоваться одновременно отображать модели «до редактирования» и «во время редактирования», перекрывающиеся (в виде призрачных линий), чтобы пользователь мог видеть изменения.

В непостоянном стиле программирования

  • Существующая структура глубоко клонирована
  • Изменения вносятся в клонированную копию.
  • Механизму отображения предписано отображать старую структуру в виде призрачных линий, а клонированную / измененную структуру - в цвете.

В неизменном стиле программирования,

  • Каждое действие пользователя, которое приводит к изменению данных, сопоставляется с последовательностью «команд».
  • Командный объект инкапсулирует, какая именно модификация должна применяться, и ссылку на исходную структуру.
    • В моем случае мой командный объект запоминает только индекс точки, который необходимо изменить, и новые координаты. (т.е. очень легкий, поскольку я не строго придерживаюсь неизменного стиля.)
  • Когда объект команды выполняется, он создает модифицированную глубокую копию структуры, делая изменение постоянным в новой копии.
  • По мере того, как пользователь вносит больше правок, будет создаваться больше объектов команд.
rwong
источник
1
Зачем делать глубокую копию неизменной структуры данных? Вам просто нужно скопировать «позвоночник» ссылок из измененного объекта в корень и сохранить ссылки на оставшиеся части исходной структуры.
Восстановите Монику
3

Преимущество глубоко неизменяемых объектов заключается в том, что для глубокого клонирования чего-либо просто необходимо скопировать ссылку. У них есть недостаток, заключающийся в том, что даже небольшое изменение глубоко вложенного объекта требует создания нового экземпляра каждого объекта, в который он вложен. У изменяемых объектов есть преимущество в том, что изменение объекта легко - просто сделайте это - но глубокое клонирование объекта требует создания нового объекта, который содержит глубокий клон каждого вложенного объекта. Хуже того, если кто-то хочет клонировать объект и вносить изменения, клонировать этот объект, вносить другие изменения и т. Д., То, независимо от того, насколько велики или малы изменения, нужно сохранять копию всей иерархии для каждой сохраненной версии состояние объекта. Насти.

Подход, который, возможно, стоит рассмотреть, заключается в определении абстрактного типа «MaybeMutable» с изменяемыми и глубоко неизменяемыми производными типами. Все такие типы будут иметь AsImmutableметод; вызов этого метода для глубоко неизменяемого экземпляра объекта просто вернет этот экземпляр. Вызов его для изменяемого экземпляра вернул бы глубоко неизменяемый экземпляр, свойства которого были глубоко неизменяемыми снимками их эквивалентов в оригинале. Неизменяемые типы с изменяемыми эквивалентами будут содержать AsMutableметод, который будет создавать изменяемый экземпляр, свойства которого соответствуют свойствам оригинала.

Изменение вложенного объекта в глубоко неизменяемом объекте потребовало бы сначала заменить внешний неизменяемый объект на изменяемый, затем заменить свойство, содержащее объект, подлежащий изменению, на изменяемый и т. Д., Но вносить повторяющиеся изменения в один и тот же аспект объекта. общий объект не потребует создания каких-либо дополнительных объектов до тех пор, пока не будет предпринята попытка вызвать AsImmutableизменяемый объект (который оставит изменяемые объекты изменяемыми, но вернет неизменные объекты, содержащие те же данные).

В качестве простых, но значительных оптимизаций каждый изменяемый объект может содержать кешированную ссылку на объект своего связанного неизменяемого типа, и каждый неизменяемый тип должен кэшировать свое GetHashCodeзначение. При вызове AsImmutableизменяемого объекта, прежде чем возвращать новый неизменный объект, убедитесь, что он соответствует кэшированной ссылке. Если это так, верните кэшированную ссылку (оставив новый неизменный объект). В противном случае обновите кэшированную ссылку, чтобы она содержала новый объект и возвращала его. Если это сделано, повторные вызовыAsImmutableбез каких-либо промежуточных мутаций будут давать те же ссылки на объекты. Даже если вы не сэкономите затраты на создание новых экземпляров, вы избежите затрат памяти на их хранение. Кроме того, сравнение на равенство между неизменяемыми объектами может быть значительно ускорено, если в большинстве случаев сравниваемые элементы являются ссылочно-равными или имеют разные хэш-коды.

Supercat
источник