Функціональне програмування - незмінність


12

Я намагаюся зрозуміти, що стосується непорушних даних у FP (зокрема, у F #, але з іншими FP також нормально) і порушувати стару звичку до повноцінного мислення (стиль OOP). Частина обраної відповіді на запитання тут ще раз підтвердила мій пошук будь-яких записів навколо проблем, які вирішуються стаціонарними представленнями в OOP з непорушними в FP (Наприклад: черга з виробниками та споживачем). Будь-які думки чи посилання вітаються? Заздалегідь спасибі.

Редагувати : Щоб уточнити питання ще трохи, як би незмінні структури (наприклад: черга) розподілялися одночасно по декількох потоках (наприклад, виробник і споживач) у ПП


Один із способів вирішення проблем з одночасністю - це робити копії черги щоразу (дещо дорого, але працює).
Робота

Відповіді:


19

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

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

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

Відповідна структура з функціональною структурою даних черги (все ще необхідною мовою, щоб вирішити одну різницю за один раз) виглядала б так:

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

Оскільки черга тепер незмінна, сам об’єкт не змінюється. У цьому псевдокоді qсама по собі є змінною; завдання q := Queue.add(…)та q := tailзмусити його вказувати на інший об’єкт. Інтерфейс функцій черги змінився: кожен повинен повернути новий об’єкт черги, що є результатом операції.

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

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

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

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

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

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

Один «промислові» мову , який отримує паралелізм right³ є Erlang . Навчання Ерлангу - це безумовно шлях до просвітництва⁴ про одночасне програмування.

Усі зараз переходять на мови, що не мають побічних ефектів!

¹ Цей термін має кілька значень; тут я думаю, що ви використовуєте це для того, щоб означати програмування без побічних ефектів, і це сенс, який я також використовую.
² Програмування з неявним станом - це імперативне програмування ; орієнтація об'єкта - це абсолютно ортогональна проблема.
³ Запальний, я знаю, але маю на увазі це. Нитки із загальною пам'яттю - це мова складання одночасного програмування. Передача повідомлень набагато простіше зрозуміти, а відсутність побічних ефектів справді світиться, як тільки ви вводите одночасність.
І це йде від того, хто не фанат Ерланг, але з інших причин.


2
+1 Набагато повніша відповідь, хоча, мабуть, можна поставити під сумнів, що Ерланг не є чистою мовою ПП.
Рейн Генріхс

1
@Rein Henrichs: Дійсно. Насправді, з усіх існуючих на сьогодні основних мов, Ерланг - це той, який найбільш вірно реалізує Орієнтацію на об'єкти.
Йорг W Міттаг

2
@ Йорг погодився. Хоча, знову ж таки, можна поставити під сумнів, що чисті FP та OO є ортогональними.
Рейн Генріхс

Отже, для реалізації незмінної черги в одночасному програмному забезпеченні повідомлення потрібно надсилати та отримувати між вузлами. Де зберігаються очікувані повідомлення?
mouviciel

Елементи черги @mouviciel зберігаються у черзі вхідних повідомлень вузла. Цей сервіс черги повідомлень є основною ознакою розподіленої інфраструктури. Альтернативна конструкція інфраструктури, яка добре працює на місцевій конкуренції, але не з розподіленими системами, полягає в блокуванні відправника, поки приймач не буде готовий. Я усвідомлюю, що це не все пояснює, для повного пояснення цього знадобиться глава чи друга книга про одночасне програмування.
Жил "ТАК - перестань бути злим"

4

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


3
Якщо це нова черга для кожної операції додавання / видалення, як би дві (або більше) операцій асинхронізації (потоків) поділили чергу? Це шаблон для абстрагування нової черги?
Венкрам

Паралельність - це зовсім інше питання. Я не можу надати достатньої відповіді в коментарі.
Рейн Генріхс

2
@Rein Henrichs: "не можна надати достатньої відповіді у коментарі". Зазвичай це означає, що вам слід оновити відповідь, щоб вирішити проблеми, пов’язані з коментарями.
С.Лотт

Паралельність також може бути одноманітною, див. Haskells Control.Concurrency.STM.
альтернатива

1
@ S.Lott в цьому випадку означає, що ОП має поставити нове запитання. Однозначність є цією проблемою, що стосується незмінних структур даних.
Рейн Генріхс

2

... проблеми, які вирішуються надзвичайними представленнями в OOP з непорушними в FP (Наприклад: черга з виробниками та споживачем)

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

Існує багато способів для кількох виробників надсилати свої результати одному спільному споживачеві. Мабуть, найбільш очевидним рішенням у F # є зробити споживача агентом (ака MailboxProcessor) та донести виробникам Postсвої результати до споживчого агента. Тут використовується внутрішня черга всередині, і це не є чистою (надсилання повідомлень у F # - це неконтрольований побічний ефект, домішка).

Однак цілком ймовірно, що основна проблема - це щось більше, як зразок розсіювання-збирання паралельного програмування. Щоб вирішити цю проблему, ви можете створити масив вхідних значень, а потім Array.Parallel.mapнад ними та зібрати результати, використовуючи послідовний Array.reduce. Крім того, ви можете використовувати функції PSeqмодуля для обробки елементів послідовностей паралельно.

Я також повинен наголосити, що в державному мисленні немає нічого поганого. Чистота має переваги, але це, звичайно, не панацея, і ви повинні також знати про її недоліки. Дійсно, саме тому F # не є чистою функціональною мовою: тому ви можете використовувати домішки, коли вони є кращими.


1

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

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

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