Функціональне програмування: правильні ідеї про одночасність та стан?


21

Прихильники ПС заявляють, що одночасність легко, оскільки їх парадигма уникає змінних станів. Я не розумію.

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

Для мене світ (поточна реальність) несе в собі цю ідею стану, і я пропускаю, як ПП це долає. Коли наш герой вживає дій, функції змінюють стан світу. Здається, що кожне рішення (ШІ чи людське) повинно базуватися на стані світу таким, яким воно є в сьогоденні. Де ми б допустили одночасність? У нас не може бути декількох процесів одночасно змінює стан світу, щоб один процес не базував свої результати на деякому стані, який минув. Мені здається, що весь контроль повинен здійснюватися в межах одного циклу управління, щоб ми завжди обробляли теперішній стан, представлений нашим поточним графіком об'єктів світу.

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

Я не бачу, наскільки корисність є корисною в моєму прикладі, і це може бути проблемою. Я можу якось неправильно представляти претензію.

Чи може хтось краще представляти цю претензію?


1
Ви посилаєтесь на загальний стан; загальний стан завжди буде таким, яким воно є, і завжди вимагатиме певної форми синхронізації, часто бажаною формою серед чистих FP людей є STM, яка дозволяє розглядати спільну пам'ять як локальну пам'ять, маючи над нею шар абстракції, який робить доступ транзакцій таким расовим умови обробляються автоматично. Ще одна техніка спільної пам’яті - це передавання повідомлень, де замість спільної пам’яті ви маєте локальну пам’ять та знання інших акторів, щоб просити їх локальну пам’ять
Джиммі Хоффа

1
Отже ... ви запитуєте, як простота одночасного використання допомагає керувати державою в однопотоковому додатку? З іншого боку, ваш приклад, очевидно, піддається концептуальній сумісності (потік для кожного суб'єкта, що контролюється АІ), незалежно від того, реалізований він чи ні. Я плутаю те, що ви тут просите.
CA McCann

1
одним словом, блискавки
jk.

2
Кожен об’єкт мав би свій погляд на світ. Буде можлива послідовність . Це, мабуть, і те, як працюють у нашому "реальному світі" при руйнуванні хвильової функції .
herzmeister

1
Цікавими можуть стати "чисто функціональні ретрогри": prog21.dadgum.com/23.html
user802500

Відповіді:


15

Спробую натякнути на відповідь. Це не відповідь, а лише вступна ілюстрація. @ jk відповідь вказує на справжню річ, блискавки.

Уявіть, у вас незмінна структура дерева. Ви хочете змінити один вузол, вставивши дитину. В результаті ви отримуєте зовсім нове дерево.

Але більшість нового дерева точно такі ж, як і старе дерево. Розумна реалізація дозволить повторно використати більшість фрагментів дерева, спрямувавши покажчики навколо зміненого вузла:

З Вікіпедії

Книга Окасакі сповнена подібних прикладів.

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

Це, мабуть, вимагає певного врахування при належному проектуванні структури світової гри даних. На жаль, я не є експертом у цих питаннях. Безумовно, це має бути щось інше, ніж матриця NxM, яку можна було б використовувати в якості змінної структури даних. Ймовірно, він повинен складатися з менших шматочків (коридори? Окремі клітини?), Які вказують один на одного, як це роблять вузли дерев.


3
+1: За вказівку на книгу Окасакі. Я не читав його, але це в моєму списку справ. Я думаю, що те, що ти зобразив, - це правильне рішення. Як альтернативу, ви можете розглянути типи унікальності (Clean, en.wikipedia.org/wiki/Uniqueness_type ): використовуючи подібний тип, ви можете деструктивно оновити об'єкти даних, зберігаючи референтну прозорість.
Джорджіо

Чи є користь, щоб відносини визначалися за допомогою непрямого посилання за допомогою ключів або ідентифікаторів? Тобто, я думав, що менше фактичних дотиків однієї структури до іншої потребуватимуть менших змін до світової структури, коли відбудеться зміна. Або ця техніка насправді не використовується в ПП?
Маріо Т. Ланза

9

Відповідь 9000 - це половина відповіді, стійкі структури даних дозволяють повторно використовувати незмінені частини.

Ви, мабуть, вже думаєте, "що, якщо я хочу змінити корінь дерева?" як це відповідає прикладу, що зараз означає зміну всіх вузлів. Тут на допомогу приходять блискавки . Вони дозволяють змінювати елемент у фокусі в O (1), а фокус можна переміщувати будь-де в структурі.

Іншим моментом у блискавках є те, що блискавка існує майже для будь-якого типу даних, який ви хочете


Я боюся, що мені знадобиться певний час, щоб заглибитися в «блискавки», оскільки я перебуваю лише на одному FP. Я не маю досвіду роботи з Haskell.
Маріо Т. Ланза

Спробую додати приклад сьогодні пізніше
jk.

4

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

Наприклад, припустимо, що Ви виконуєте рішення AI незалежно одне від одного і в жодному конкретному порядку. Вони не по черзі, всі приймають рішення одночасно, і тоді світ прогресує. Код може виглядати приблизно так:

func MakeMonsterDecision curWorldState monster =
    ...
    ...
    return monsterDecision

func NextWorldState curWorldState =
    ...
    let monsterMakeDecisionForCurrentState = MakeMonsterDecision curWorldState
    let monsterDecisions = List.map monsterMakeDecisionForCurrentState activeMonsters
    ...
    return newWorldState

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

Імперативною мовою ви, швидше за все, повторятиметеся над кожним монстром, застосовуючи їх вплив на світ. Просто зробити це простіше, тому що ви не хочете мати справу з клонуванням або складним випромінюванням. У цьому випадку компілятор не може виконувати обчислення монстра паралельно, оскільки ранні рішення монстра впливають на більш пізні рішення монстра.


Це дуже допомагає. Я бачу, як в грі буде велика користь для того, щоб монстри одночасно вирішували, що робити далі.
Маріо Т. Ланза

4

Прослуховування кілька Річа Хіккі переговорів - це одне , зокрема - полегшено моє замішання. В одному він зазначив, що добре, що паралельні процеси можуть мати не самий поточний стан. Мені потрібно було це почути. У мене виникли труднощі з перетравленням, що програми насправді не в порядку з прийняттям рішень на основі знімків світу, які з тих пір були замінені новими. Мені було цікаво, як паралельна ПП обходилася питанням обґрунтування рішень на старій державі.

У банківській програмі ми ніколи не хотіли б базувати рішення на огляді стану, яке з тих пір було замінено новим (відбулося відкликання).

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


0

Прихильники ПС заявляють, що одночасність легко, оскільки їх парадигма уникає змінних станів. Я не розумію.

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

Серійні алгоритми

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

Легша паралельність

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

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

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

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

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

Побічні ефекти

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

[...] тому що їх парадигма уникає змінних станів.

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

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


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

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

1
Більшість програм, які мають справу з мутаційним станом, роблять це через те, що їм доводиться з тих чи інших причин. І більшість програм не є помилковими, оскільки вони використовують спільний стан або аномально оновлюють його - зазвичай це тому, що вони отримують несподіване сміття як вхід (який визначає вихід сміття) або через те, що вони неправильно працюють на своїх входах (неправильно в сенсі призначення досягти). Функціональних зразків це мало для вирішення цього питання.
Стів

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

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