Незмінні структури та ієрархія глибокого складу


9

Я розробляю додаток для графічного інтерфейсу, активно працюю з графікою - заради цього можна думати про це як векторний редактор. Дуже спокусливо зробити всі структури даних непорушними - так що я міг би скасувати / повторити, скопіювати / вставити та багато інших речей майже без зусиль.

Для простоти я скористаюся наступним прикладом - додаток використовується для редагування полігональних фігур, тому у мене є об'єкт "Полігон", який є просто списком незмінних точок:

Scene -> Polygon -> Point

І тому у мене в програмі є лише одна змінна змінна - та, яка містить поточний об’єкт Scene. Проблема, яка у мене виникає, коли я намагаюся реалізувати перетягування точок - у змінній версії я просто захоплюю Pointоб’єкт і починаю змінювати його координати. У незмінному варіанті - я застряг. Я міг би зберігати індекси Polygonв поточному Scene, індекс затягнутої точки Polygonі замінювати його кожен раз. Але такий підхід не має масштабів - коли рівень складу піде на 5 і далі, котельня стане нестерпною.

Я впевнений, що цю проблему можна вирішити - адже є Haskell з повністю незмінними структурами та IO монадою. Але я просто не можу знайти, як.

Чи можете ви підказати мені?


@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переглядаєте 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 рівнями композиції). Основна ідея - клонувати, а потім модифікувати . У незмінному стилі програмування клонування та модифікація мають відбуватися разом, що стає об'єктом команд .

Зауважте, що в стилі програмування, що змінюється, клонування все-таки було б необхідним:

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

У стилі програмування, що змінюється,

  • Існуюча структура є глибоко клонованою
  • Зміни вносяться в клоновану копію
  • Двигун дисплея повідомляється про те, щоб стару структуру виводити в привидні лінії, а клоновану / модифіковану структуру - в кольорі.

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

  • Кожна дія користувача, яка призводить до зміни даних, відображається у послідовності "команд".
  • Командний об'єкт інкапсулює, яку саме модифікацію слід застосувати, та посилання на початкову структуру.
    • У моєму випадку мій командний об'єкт запам'ятовує лише точковий індекс, який потрібно змінити, та нові координати. (тобто дуже легкий, оскільки я не суворо дотримуюся непорушного стилю.)
  • Коли виконується командний об'єкт, він створює змінену глибоку копію структури, роблячи модифікацію постійною в новій копії.
  • Оскільки користувач вносить більше змін, буде створено більше командних об’єктів.

1
Навіщо робити глибоку копію незмінної структури даних? Вам просто потрібно скопіювати "хребет" посилань з модифікованого об'єкта в корінь і зберегти посилання на інші частини оригінальної структури.
Відновіть Моніку

3

Глибоко непорушні об'єкти мають перевагу в тому, що глибоке клонування чогось просто вимагає копіювання посилання. Вони мають недолік, що навіть для невеликої зміни глибоко вкладеного об'єкта потрібно побудувати новий примірник кожного об'єкта, в якому він вкладений. Переважні об'єкти мають перевагу в тому, що змінювати об’єкт легко - просто зробіть це, - але глибоке клонування об'єкта вимагає побудови нового об'єкта, який містить глибокий клон кожного вкладеного об'єкта. Гірше, якщо хтось хоче клонувати об’єкт і внести зміни, клонувати його, внести ще одну зміну тощо, то незалежно від того, наскільки великі чи маленькі зміни, потрібно зберігати копію всієї ієрархії для кожної збереженої версії стан об'єкта. Неприємний.

Підхід, який, можливо, варто розглянути, було б визначити абстрактний тип "можливоMutable" із змінними та глибоко незмінними типами похідних. Усі такі типи мали б AsImmutableметод; виклик цього методу на глибоко незмінному екземплярі об'єкта просто поверне цей екземпляр. Викликаючи його на мутаційному екземплярі, повертався б екземпляр, що не змінювався глибоко, незмінним, властивості якого були оригінальними знімків глибоко незмінних еквівалентів. Незмінні типи з мутаційними еквівалентами спортували б AsMutableметод, який побудував би мутаційний екземпляр, властивості якого збігалися з оригіналом.

Зміна вкладеного об'єкта в глибоко незмінний об'єкт потребує спочатку заміни зовнішнього незмінного об'єкта на змінний, а потім заміни властивості, що містить предмет, що підлягає зміні, на змінний тощо, але внесення повторних змін у той же аспект загальний об'єкт не потребує внесення додаткових об'єктів до тих пір, поки не буде зроблена спроба викликати AsImmutableоб'єкт, що змінюється (який би залишив об'єкти, що змінюються, змінити, але повернути незмінні об'єкти, що містять ті самі дані).

Як прості, але значні оптимізації, кожен змінний об'єкт може містити кешоване посилання на об'єкт його асоційованого непорушного типу, і кожен незмінний тип повинен кешувати його GetHashCodeзначення. Перш AsImmutableніж повертати новий незмінний об'єкт, переконайтеся, що він відповідає кешованому посиланню. Якщо так, поверніть кешоване посилання (відмовившись від нового незмінного об'єкта). В іншому випадку оновіть кешоване посилання, щоб утримувати новий об’єкт, і поверніть його. Якщо це зроблено, повторні дзвінки доAsImmutableбез будь-яких втручаються мутацій дасть однакові посилання на об'єкт. Навіть якщо ви не заощадите затрати на створення нових екземплярів, уникнете вартості пам'яті на їх збереження. Крім того, порівняння рівностей між незмінними об'єктами може бути значно пришвидшеним, якщо в більшості випадків елементи, що порівнюються, є рівними посиланнями або мають різні хеш-коди.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.