Я читаю і чую, що люди (також на цьому веб-сайті) звичайно вихваляють парадигму функціонального програмування, підкреслюючи, наскільки добре мати все незмінне. Зокрема, люди пропонують такий підхід навіть у традиційно необхідних мовах OO, таких як C #, Java або C ++, а не лише в суто функціональних мовах, таких як Haskell, які змушують це застосувати програміста.
Мені важко зрозуміти, тому що я вважаю незмінність та побічні ефекти ... зручними. Однак, враховуючи те, як люди в даний час засуджують побічні ефекти і вважають гарною практикою позбутися від них, де це можливо, я вважаю, що якщо я хочу бути грамотним програмістом, я повинен почати своє бажання до кращого розуміння парадигми ... Звідси мій Q.
Одне місце, коли я знаходжу проблеми з функціональною парадигмою, це те, коли на об'єкт природно посилаються з декількох місць. Дозвольте описати це на двох прикладах.
Першим прикладом буде моя гра C #, яку я намагаюся зробити у вільний час . Це покрокова веб-гра, в якій обидва гравці мають команди по 4 монстри і можуть відправити монстра зі своєї команди на поле битви, де вона зіткнеться з монстром, надісланим противником. Гравці також можуть відкликати монстрів з поля бою та замінити їх іншим монстром зі своєї команди (подібно Покемону).
У цій обстановці на одного монстра природним чином можна віднести щонайменше 2 місця: команду гравця та поле битви, на яке посилаються два "активні" монстри.
Тепер розглянемо ситуацію, коли одне чудовисько потрапило і втрачає 20 балів здоров’я. У дужках імперативної парадигми я модифікую health
поле цього монстра, щоб відобразити цю зміну - і це я зараз роблю. Однак це робить Monster
клас мінливим і пов'язані з ним функції (методи) нечистими, що, напевно, вважається поганою практикою на даний момент.
Незважаючи на те, що я дав собі дозвіл мати код цієї гри в меншому, ніж ідеальному стані, щоб мати будь-які надії фактично закінчити її в якийсь момент майбутнього, я хотів би знати і розуміти, як це має бути написано правильно. Тому: Якщо це вада дизайну, як це виправити?
У функціональному стилі, наскільки я його розумію, я б замість цього зробив копію цього Monster
об'єкта, зберігаючи його ідентичний старому, за винятком цього поля; і метод suffer_hit
поверне цей новий об'єкт замість зміни старого на місце. Тоді я б також скопіював Battlefield
об’єкт, зберігаючи всі його поля однакові, крім цього монстра.
Це виникає щонайменше з двома труднощами:
- Ієрархія може бути набагато глибшою, ніж цей спрощений приклад просто
Battlefield
->Monster
. Мені довелося б зробити таке копіювання всіх полів, крім одного, і повернути новий об’єкт аж до цієї ієрархії. Це був би код котла, який мені дратує, тим більше, що функціональне програмування передбачає зменшення котла. - Набагато гострішою проблемою є те, що це призведе до того, що дані не синхронізуються . Польовий активний монстр побачив би його здоров'я; однак, цей самий монстр, на який посилається його контрольний гравець
Team
, не буде. Якби я замість цього застосував імперативний стиль, кожна модифікація даних була б миттєво помітна з усіх інших місць коду, і в таких випадках, як цей, мені здається, що це дуже зручно - але те, як я отримую речі, це саме те, що кажуть люди неправильно з імперативним стилем!- Тепер можна було б опікуватися цим питанням, здійснюючи подорож до наступної
Team
атаки. Це зайва робота. Однак, що робити, якщо монстр може раптом пізніше посилатися з ще більшої кількості місць? Що робити, якщо я маю здібності, які, наприклад, дозволяють монстру зосередитись на іншому монстрі, який не обов'язково знаходиться на полі (я насправді розглядаю таку здатність)? Чи зможу чи я , звичайно , що не забудьте взяти також подорож в сфокусовані монстр миттєвого після кожної атаки? Здається, це бомба тимчасового вибуху, яка вибухне, коли код стане складнішим, тому я думаю, що це рішення не підлягає.
- Тепер можна було б опікуватися цим питанням, здійснюючи подорож до наступної
Ідея кращого рішення походить з мого другого прикладу, коли я потрапив у ту ж саму проблему. У академіях нам сказали написати перекладача мови власного дизайну в Haskell. (Це також я був змушений почати розуміти, що таке ПП). Проблема виявилася, коли я впроваджував закриття. Знову на той самий діапазон тепер можна посилатися з декількох місць: Через змінну, яка містить цю область і як батьківську область будь-яких вкладених областей! Очевидно, що якщо зміна в цій області здійснюється через будь-яку з посилань, що вказують на неї, ця зміна також має бути видимою через усі інші посилання.
Я вирішив привласнити ідентифікатор кожної області і провести центральний словник усіх областей State
монади. Тепер змінні міститимуть лише ідентифікатор області, до якої вони були прив'язані, а не сам діапазон, а вкладені діапазони також міститимуть ідентифікатор своєї батьківської області.
Я здогадуюсь, що такий же підхід можна було б спробувати в моїй грі з монстрами ... Поля та команди не посилаються на монстрів; натомість вони містять ідентифікатори монстрів, які зберігаються в центральному словнику монстрів.
Однак я знову можу побачити проблему з таким підходом, який заважає мені без вагань сприймати його як рішення проблеми:
Це ще раз є джерелом кодової панелі. Це робить однолінійні обов'язково 3-х вкладишами: те, що раніше було однорядковим на місці модифікацією одного поля, тепер вимагає (a) Вилучення об'єкта з центрального словника (b) Внесення змін (c) Збереження нового об'єкта до центрального словника. Крім того, зберігання ідентифікаторів об'єктів та центральних словників замість посилань збільшує складність. Оскільки FP рекламується для зменшення складності та кодового коду, це натякає на те, що я роблю це неправильно.
Я також збирався писати про асекундну проблему, яка здається набагато серйознішою: такий підхід вводить витоки пам'яті . Об'єкти, які недоступні, зазвичай збирають сміття. Однак об'єкти, що знаходяться в центральному словнику, не можуть бути зібрані сміттям, навіть якщо жоден доступний об'єкт не посилається на цей конкретний ідентифікатор. І хоча теоретично ретельне програмування може уникнути витоку пам'яті (ми можемо подбати про те, щоб вручну видалити кожен об'єкт із центрального словника, як тільки він більше не потрібен), це схильне до помилок і FP рекламується для підвищення правильності програм, щоб знову це зробити не правильно.
Однак я вчасно з’ясував, що це швидше здається вирішеною проблемою. Java передбачає, WeakHashMap
що можна було б вирішити цю проблему. C # надає аналогічну програму - ConditionalWeakTable
- хоча згідно з документами вона призначена для використання компіляторами. І в Haskell у нас є System.Mem.Weak .
Чи зберігання таких словників є правильним функціональним рішенням цієї проблеми чи є більш просте, яке я не бачу? Я думаю, що кількість таких словників може легко зростати і погано; тому якщо ці диктатури також повинні бути непорушними, це може означати багато параметрів або, мов, що це підтримують, монадичних обчислень, оскільки словники проводитимуться в монадах (але ще раз я читаю, що це суто функціонально Мови якомога менше коду повинні бути монадичними, тоді як це рішення словника розмістить майже весь код всередині State
монади; це ще раз змушує мене сумніватися, чи це правильне рішення.)
Після деякого розгляду, я думаю, я би додав ще одне запитання: що ми отримуємо, будуючи такі словники? Те, що не так з імперативним програмуванням, на думку багатьох експертів, те, що зміни в деяких об'єктах поширюються на інші фрагменти коду. Для вирішення цього питання об'єкти повинні бути незмінними - саме тому, якщо я правильно розумію, що зміни, внесені до них, не повинні бути помітні в іншому місці. Але зараз я стурбований іншими фрагментами коду, що працює над застарілими даними, тому я вигадую центральні словники, щоб ... вкотре зміни деяких фрагментів коду поширюються на інші фрагменти коду! Чи не повертаємось ми до імперативного стилю з усіма його передбачуваними недоліками, але з додатковою складністю?
Team
) можуть отримати результат битви, і, таким чином, стану монстрів за допомогою кортежу (номер бою, ідентифікатор монстра).