Використання стійких структур даних нефункціональними мовами


17

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

Але час від часу ми бачимо бібліотеки стійких структур даних для (державних, OOP) мов, таких як Java. Твердження, яке часто лунає на користь стійких структур даних, полягає в тому, що оскільки вони незмінні, вони є безпечними для потоків .

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

Але оскільки різні потоки бачать різні стани даних, здається, що стійкі структури даних самі по собі не є достатніми для обробки сценаріїв, коли один потік вносить зміни, видимі для інших потоків. Для цього, здається, ми повинні використовувати такі пристрої, як атоми, посилання, транзакційна пам'ять програмного забезпечення або навіть класичні блокування та механізми синхронізації.

Чому ж тоді, незмінність PDS розцінюється як щось корисне для "безпеки ниток"? Чи є реальні приклади, коли PDS допомагають у синхронізації чи вирішенні проблем одночасності? Або PDS - це просто спосіб надання інтерфейсу без стану об'єкту для підтримки функціонального стилю програмування?


3
Ви продовжуєте говорити "наполегливо". Ви дійсно маєте на увазі "стійкий", як "здатний пережити перезапуск програми", або просто "незмінний", як у "ніколи не змінюється після його створення"?
Кіліан Фот

17
@KilianFoth Стійкі структури даних мають чітко встановлене визначення : "стійка структура даних - це структура даних, яка завжди зберігає попередню версію себе при зміні". Тож мова йде про повторне використання попередньої структури, коли на її основі створюється нова структура, а не стійкість, як в "здатному пережити перезапуск програми".
Michał Kosmulski

3
Здається, ваше запитання менше стосується використання стійких структур даних у нефункціональних мовах та більше про те, які частини паралельності та паралелізму не вирішуються ними, незалежно від парадигми.

Моя помилка. Я не знав, що "стійка структура даних" є технічним терміном, відмінним від простої стійкості.
Кіліан Фот

@delnan Так, це правильно.
Ray Toal

Відповіді:


15

Стійкі / незмінні структури даних не вирішують проблеми одночасності самостійно, але вони значно полегшують їх вирішення.

Розглянемо нитку Т1, яка передає набір S іншому потоку Т2. Якщо S є змінним, T1 має проблему: він втрачає контроль над тим, що відбувається з S. Нитка T2 може змінити його, тому T1 взагалі не може покладатися на вміст S. І навпаки - T2 не може бути впевнений, що T1 не змінює S, поки T2 працює на ньому.

Одне рішення полягає в тому, щоб додати якийсь контракт до зв'язку T1 і T2, щоб тільки одному з потоків було дозволено змінювати S. Це схильне до помилок і обтяжує як дизайн, так і реалізацію.

Іншим рішенням є те, що T1 або T2 клонують структуру даних (або обидва вони, якщо вони не узгоджені). Однак якщо S не є стійким, це дорогий O (n) операція .

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

Дивіться також: стійка та незмінна структура даних .


2
Так, "безпека потоку" в цьому контексті просто означає, що одному потоці не потрібно турбуватися про те, щоб інші потоки знищували дані, які вони бачать, але не мають нічого спільного з синхронізацією та обробкою даних, якими ми хочемо поділитися між потоками. Це відповідає тому, що я думав, але +1 для елегантного твердження "не вирішувати проблеми з валютою самостійно".
Ray Toal

2
@RayToal Так, в цьому контексті "безпека потоків" означає саме це. Як розподіляються дані між потоками - це інша проблема, яка, як ви вже згадували, має безліч рішень (особисто мені подобається STM за її сумісність). Безпека нитки гарантує, що вам не доведеться турбуватися про те, що відбувається з даними після обміну. Це насправді велика справа, оскільки потокам не потрібно синхронізувати того, хто працює над структурою даних та коли.
Петро Пудлак

@RayToal Це дозволяє вишукані паралельні моделі, такі як дійові особи , які позбавляють розробників від того, щоб мати справу з явним блокуванням та керуванням потоками, і які покладаються на незмінність повідомлень - ви не знаєте, коли повідомлення надсилається та обробляється, або до чого іншого акторів, на які він пересилається.
Петро Пудлак

Спасибі Петре, я ще раз погляну акторів. Мені знайомі всі механізми Clojure, і я зазначив, що Річ Хікі явно вирішив не використовувати акторську модель , принаймні, як це показано в Ерланге. І все-таки, чим більше ви знаєте, тим краще.
Ray Toal

@RayToal Цікаве посилання, дякую. Я використовував лише акторів як приклад, не те, що я кажу, що це було б найкращим рішенням. Я не використовував Clojure, але здається, що його кращим рішенням є STM, який я, безумовно, віддаю перевагу перед акторами. STM також покладається на стійкість / незмінність - неможливо було б перезапустити транзакцію, якби вона безповоротно змінила структуру даних.
Петро Пудлак

5

Чому ж тоді, незмінність PDS розцінюється як щось корисне для "безпеки ниток"? Чи є реальні приклади, коли PDS допомагають у синхронізації чи вирішенні проблем одночасності?

Основна перевага PDS у цьому випадку полягає в тому, що ви можете змінювати частину даних, не роблячи все унікальним (без глибокого копіювання, так би мовити). Це має багато потенційних переваг, окрім того, що ви можете писати дешеві функції без побічних ефектів: екземпляри копіювання та вставлення даних, тривіальні системи скасування, функції тривіального відтворення в іграх, тривіальне неруйнівне редагування, безпека дрібницьких винятків тощо тощо.


2

Можна уявити структуру даних, яка була б стійкою, але незмінною. Наприклад, ви можете взяти зв'язаний список, представлений вказівником на перший вузол, і попередньою операцією, яка б повернула новий список, що складається з нового головного вузла плюс попереднього списку. Оскільки ви все ще маєте посилання на попередню главу, ви можете отримати доступ до цього списку та змінити його, який тим часом також вбудований у новий список. Хоча це можливо, така парадигма не дає переваг стійких та незмінних структур даних, наприклад, вона, звичайно, не є безпечною для потоків. Однак це може використовувати його до тих пір, поки розробник знає, що робить, наприклад, для економії простору. Також зауважте, що, хоча структура може змінюватися на мовному рівні, оскільки ніщо не заважає коду змінити її,

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

Що стосується прикладів з нефункціональних мов, Java String.substring()використовує те, що я б назвав стійкою структурою даних. Рядок представлений масивом символів плюс початкове та кінцеве зміщення діапазону масиву, який фактично використовується. Коли створена підрядка, новий об’єкт повторно використовує той самий масив символів, лише із зміненими зміщеннями початку та кінця. Оскільки Stringє незмінним, це (щодо substring()операції, а не інших) незмінна стійка структура даних.

Незмінність структур даних є частиною, що стосується безпеки потоку. Їх наполегливість (повторне використання існуючих фрагментів при створенні нової структури) має відношення до ефективності роботи з такими колекціями. Оскільки вони незмінні, така операція, як додавання елемента, не змінює існуючу структуру, а повертає нову, додаючи додатковий елемент. Якщо кожен раз копіювати всю структуру, починаючи з порожньої колекції і додаючи 1000 елементів по одному, щоб закінчити колекцію 1000 елементів, створили б тимчасові об'єкти з 0 + 1 + 2 + ... + 999 = Всього 500000 елементів, які були б величезними витратами. За допомогою стійких структур даних цього можна уникнути, оскільки 1-елементний збір повторно використовується у двоелементному, який повторно використовується у 3-елементному тощо,


Іноді корисно мати квазінезмінні об'єкти, у яких всі, окрім одного аспекту стану, незмінні: здатність зробити об’єкт, стан якого майже схожий на даний об’єкт. Наприклад, AppendOnlyList<T>підкріплений масивом зростаючих масивів може створювати незмінні знімки без необхідності копіювання будь-яких даних для кожного знімка, але не можна було створити список, який містив би вміст такого знімка, плюс новий елемент, без повторної копії все до нового масиву.
supercat

0

Я, правда кажучи, упереджений як один, хто застосовує такі поняття в C ++ мовою та її природою, а також моїм доменом і навіть тим, як ми використовуємо цю мову. Але з огляду на ці речі, я думаю, що незмінні конструкції є найменш цікавим аспектом, коли мова йде про отримання переваг, пов'язаних з функціональним програмуванням, як-от безпека потоку, простота міркувань щодо системи, пошук більшого використання функцій (і пошук того, що ми можемо поєднуйте їх у будь-якому порядку без неприємних сюрпризів) тощо.

Візьмемо цей спрощений приклад C ++ (правда, не оптимізовано для простоти, щоб не бентежити себе перед будь-якими фахівцями з обробки зображень):

// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
     Image dst(new_w, new_h);
     for (int y=0; y < new_h; ++y)
     {
         for (int x=0; x < new_w; ++x)
              dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
     }
     return dst;
}

Хоча реалізація цієї функції мутує місцевий (і тимчасовий) стан у вигляді двох змінних лічильників та тимчасового локального зображення для виведення, воно не має зовнішніх побічних ефектів. Він вводить зображення і виводить нове. Ми можемо його багатопрочитати до змісту серця. Це легко міркувати, легко ретельно перевірити. Це безпечно для винятків, оскільки якщо щось кинеться, нове зображення автоматично відкидається, і нам не потрібно турбуватися про відкат зовнішніх побічних ефектів (зовнішніх зображень не змінюється поза межами функції, так би мовити).

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

Чистота

Тож чисті функції (без зовнішніх побічних ефектів) мені дуже цікаві, і я підкреслюю важливість часто віддавати перевагу членам команди навіть у С ++. Але незмінні конструкції, застосовані як правило, відсутні контекст і нюанс, не є для мене майже такими цікавими, оскільки, враховуючи імперативний характер мови, часто корисно і практично можна міняти деякі локальні тимчасові об'єкти в процесі ефективної роботи (обидва для розробника та апаратних засобів) реалізація чистої функції.

Дешеве копіювання потужних конструкцій

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

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

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

Незмінюваність

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

І хоча я вважаю це найменш цікавим з цих трьох мов, таких як C ++, він, безумовно, може спростити тестування та безпеку потоку та міркування нетривіальних об'єктів. Працювати з гарантією того, що об'єкту не може бути надана будь-яка унікальна комбінація стану поза його конструктором, може бути, і це може бути завантаженням, наприклад, і що ми можемо вільно передавати його навколо, навіть за посиланням / покажчиком, не спираючись на невимушеність і читання- лише ітератори та ручки тощо, гарантуючи (ну, принаймні, наскільки ми можемо в рамках мови), що його оригінальний вміст не буде змінено.

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

Нарешті:

[...] Здавалося б, стійкі структури даних самі по собі не є достатніми для обробки сценаріїв, коли один потік вносить зміни, видимі іншим потокам. Для цього, здається, ми повинні використовувати такі пристрої, як атоми, посилання, транзакційна пам'ять програмного забезпечення або навіть класичні блокування та механізми синхронізації.

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

Але я виявив, що коли ви отримаєте такий тип копіювання та можливість вивести частково модифіковані версії здоровенних структур, що забруднюються дешево, як це можна отримати зі стійкими структурами даних, наприклад, це часто відкриває багато дверей та можливостей не замислювався раніше про паралелізацію коду, який може працювати повністю незалежно один від одного в строгому паралельному трубопроводі вводу / виводу. Навіть якщо деякі частини алгоритму мають мати послідовний характер, ви можете відкласти цю обробку на одну нитку, але виявите, що, спираючись на ці поняття, відкрив двері, щоб легко і без побоювань паралелізувати 90% здоровенної роботи, наприклад

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