Я відчуваю, що побічні ефекти - це природне явище. Але це щось на зразок табу у функціональних мовах. Які причини?
Моє питання стосується функціонального стилю програмування. Не всі мови програмування / парадигми.
Я відчуваю, що побічні ефекти - це природне явище. Але це щось на зразок табу у функціональних мовах. Які причини?
Моє питання стосується функціонального стилю програмування. Не всі мови програмування / парадигми.
Відповіді:
Запис ваших функцій / методів без побічних ефектів - тож вони чисті функції - полегшує міркування про правильність вашої програми.
Це також дозволяє легко складати ці функції для створення нової поведінки.
Це також робить можливими певні оптимізації, коли компілятор може, наприклад, запам'ятати результати функцій або використовувати загальне усунення субекспресії.
Змінити: за запитом Benjol в: Тому що багато ваш стан зберігається в стеку (потік даних, не контролює потік, як Іона назвав його тут ), ви можете parallelise або іншим чином змінювати порядок виконання цих частин вашого обчислення , що не залежать від один одного. Ви можете легко знайти ці незалежні частини, оскільки одна частина не забезпечує введення іншої.
У середовищах з відладчиками, які дозволяють повернути стек і відновити обчислення (наприклад, Smalltalk), наявність чистих функцій означає, що ви можете легко побачити, як змінюється значення, оскільки попередні стани доступні для перевірки. У важкому для мутації обчисленні, якщо ви явно не додаєте дії do / unndo до вашої структури чи алгоритму, ви не можете бачити історію обчислень. (Це пов'язано з першим пунктом: написання чистих функцій полегшує перевірку правильності вашої програми.)
Зі статті про функціональному програмуванні :
На практиці програми повинні мати деякі побічні ефекти. Саймон Пейтон-Джонс, головний учасник мови функціонального програмування Haskell, сказав наступне: "Зрештою, будь-яка програма повинна маніпулювати станом. Програма, яка не має жодних побічних ефектів, є різновидом чорного поля. Все, що ви можете сказати, це що коробка стає гарячішою ". ( Http://oscon.blip.tv/file/324976 ) Ключ обмежити побічні ефекти, чітко визначити їх і уникати їх розсіювання в коді.
Ви помилилися, функціональне програмування сприяє обмеженню побічних ефектів, щоб зробити програми зрозумілими та оптимізованими. Навіть Haskell дозволяє записувати у файли.
По суті, я говорю, що функціональні програмісти не вважають, що побічні ефекти є злими, вони просто думають, що обмежити використання побічних ефектів добре. Я знаю, що це може здатися такою простою відмінністю, але все це має значення.
readFile
, це визначення послідовності дій. ця послідовність функціонально чиста і на зразок абстрактного дерева, що описує, що робити. фактичні брудні побічні ефекти потім виконуються під час виконання.
Кілька приміток:
Функції без побічних ефектів можуть тривіально виконуватись паралельно, тоді як функції із побічними ефектами, як правило, потребують певної синхронізації.
Функції без побічних ефектів дозволяють здійснити більш агресивну оптимізацію (наприклад, прозоро використовуючи кеш результату), оскільки, поки ми отримаємо правильний результат, навіть не має значення, чи справді виконувалася функція чи ні
deterministic
положення про функції без побічних ефектів, тому вони не виконуються частіше, ніж потрібно.
deterministic
Положення тільки ключове слово , яке повідомляє компілятору , що це є детермінованою функцією, яку можна порівняти з тим, як final
ключове слово в Java повідомляє компілятору , що змінна не може змінитися.
Я в основному працюю над функціональним кодом зараз, і з цього погляду це здається сліпуче очевидним. Побічні ефекти створюють величезне психічне навантаження на програмістів, які намагаються прочитати та зрозуміти код. Ви не помічаєте цього тягаря, поки не звільниться від нього деякий час, а потім раптом доведеться знову читати код із побічними ефектами.
Розглянемо цей простий приклад:
val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.
// Code you are troubleshooting
// What's the expected value of foo here?
У функціональній мові я знаю, що foo
це все ще 42. Мені навіть не доводиться дивитися на код між ними, тим більше розуміти його чи дивитися на реалізацію функцій, які він викликає.
Все, що стосується паралельності, паралелізації та оптимізації, є приємним, але це те, що комп'ютерні працівники розміщують у брошурі. Не потрібно дивуватися, хто мутує вашу змінну і коли це те, що мені дуже подобається в щоденній практиці.
Мало на яких мовах неможливо викликати побічні ефекти. Мови, повністю вільні від побічних ефектів, було б надзвичайно важким (майже неможливим) для використання, за винятком обмеженої кількості.
Чому побічні ефекти вважаються злими?
Тому що їм набагато складніше міркувати про те, що саме робить програма, і доводити, що вона робить те, що ти очікуєш.
На дуже високому рівні уявіть тестування всього 3-х рівневого веб-сайту лише за допомогою тестування в чорному ящику. Звичайно, це можливо, залежно від масштабу. Але, безумовно, відбувається багато дублювання. І якщо є помилка (це пов’язано з побічним ефектом), ви могли потенційно зламати всю систему для подальшого тестування, поки помилка не буде діагностована та виправлена, і виправлення не буде розгорнуто до тестового середовища.
Переваги
Тепер зменшіть масштаб. Якби ви досить добре писали безкоштовний код із побічними ефектами, наскільки швидше ви б міркували над тим, що зробив якийсь існуючий код? На скільки швидше ви могли написати одиничні тести? Наскільки впевнено ви відчуваєте , що код без будь - яких побічних ефектів був гарантований помилкою безкоштовно, і що користувачі можуть обмежити їх вплив на будь-які помилки це було їсти?
Якщо код не має побічних ефектів, компілятор також може мати додаткові оптимізації, які він може виконувати. Реалізувати ці оптимізації може бути набагато простіше. Це може бути набагато простіше навіть концептуалізувати оптимізацію вільного коду з побічними ефектами, а це означає, що ваш постачальник компілятора може впровадити оптимізацію, важко неможливу в коді з побічними ефектами.
Паралельність також значно простіша у впровадженні, автоматичному генеруванні та оптимізації, коли код не має побічних ефектів. Це тому, що всі твори можна сміливо оцінити в будь-якому порядку. Дозвіл програмістів писати дуже одночасний код широко вважається наступною великою проблемою, з якою потрібно боротися з комп’ютерних наук, і однією з небагатьох залишків хеджування проти Закону Мура .
Побічні ефекти - це як "протікання" у вашому коді, які потрібно буде вирішити пізніше, або ви, або хтось, хто не підозрює колегу.
Функціональні мови уникають змінних станів та змінних даних, як спосіб зробити код менш залежним від контексту та більш модульним. Модульність гарантує, що робота одного розробника не впливатиме на роботу іншого.
Швидкість масштабування розробок з розміром команди - сьогодні "святий граал" розробки програмного забезпечення. Під час роботи з іншими програмістами мало що важливо, як і модульність. Навіть найпростіші з логічних побічних ефектів роблять співпрацю надзвичайно важкою.
Ну, ІМХО, це досить лицемірно. Ніхто не любить побічні ефекти, але всі їм потрібні.
Що таке небезпечні побічні ефекти, це те, що якщо ви викликаєте функцію, то це, можливо, впливає не тільки на те, як функція поводиться, коли вона викликається наступного разу, але, можливо, це вплине і на інші функції. Таким чином, побічні ефекти призводять до непередбачуваної поведінки та нетривіальних залежностей.
Парадигми програмування, такі як ОО та функціональні, вирішують цю проблему. ОО зменшує проблему, накладаючи розділення проблем. Це означає, що стан програми, який складається з безлічі змінних даних, інкапсульований у об'єкти, кожен з яких відповідає за підтримку лише власного стану. Таким чином знижується ризик залежностей, а проблеми набагато більш відокремлені та простіші.
Функціональне програмування має набагато більш радикальний підхід, коли стан програми просто незмінний з точки зору програміста. Це гарна ідея, але робить мову самостійно марною. Чому? Оскільки будь-яка операція вводу / виводу має побічні ефекти. Щойно ви читаєте з будь-якого потоку введення, стан програми, швидше за все, зміниться, оскільки наступного разу, коли ви будете викликати ту саму функцію, результат, ймовірно, буде іншим. Можливо, ви читаєте різні дані, або - також можливість - операція може не вдатися. Те саме стосується виводу. Рівномірний вихід - це операція з побічними ефектами. Це не те, що ви часто розумієте в наші дні, але уявіть, що у вас є лише 20 Кб для вашого виходу, і якщо ви виходите більше, ваш додаток виходить з ладу, оскільки у вас немає місця на диску або що завгодно.
Так, так, побічні ефекти є неприємними та небезпечними з точки зору програміста. Більшість помилок походять від того, як певні частини стану програми перемикаються майже незрозуміло, через непродумані та часто непотрібні побічні ефекти. З точки зору користувача, побічні ефекти - це сенс використання комп'ютера. Їм не байдуже, що відбувається всередині або як це організовано. Вони щось роблять і очікують, що комп'ютер змінить відповідно.
Будь-який побічний ефект вводить додаткові параметри вводу / виводу, які необхідно враховувати при тестуванні.
Це робить валідацію коду набагато складнішою, оскільки середовище не може бути обмежене лише валідизованим кодом, але повинно вносити деяке або все навколишнє середовище (глобальний, який оновлюється, живе в цьому коді там, що, в свою чергу, залежить від цього код, який, у свою чергу, залежить від того, щоб жити на повному сервері Java EE ....)
Намагаючись уникнути побічних ефектів, ви обмежуєте кількість екстерналізму, необхідного для запуску коду.
На мій досвід, хороший дизайн в об'єктно-орієнтованому програмуванні передбачає використання функцій, які мають побічні ефекти.
Наприклад, візьміть основний настільний додаток для інтерфейсу користувача. У мене може бути запущена програма, яка має на своїй купі об’єктний графік, що представляє поточний стан доменної моделі моєї програми. Повідомлення надходять до об'єктів цього графіка (наприклад, за допомогою викликів методів, викликаних від контролера рівня інтерфейсу користувача). Об'єктний графік (модель домену) на купі змінюється у відповідь на повідомлення. Спостерігачі моделі інформуються про будь-які зміни, користувальницький інтерфейс та, можливо, інші ресурси змінюються.
Далеко від зла, правильне розташування цих побічних ефектів, що змінюють купу та екрани, лежить в основі дизайну ОО (в даному випадку - модель MVC).
Звичайно, це не означає, що ваші методи повинні мати довільні побічні ефекти. І функції, що не мають побічних ефектів, мають місце в поліпшенні читаемості, а іноді і продуктивності вашого коду.
Як зазначено вище, функціональні мови не настільки сильно запобігають коду від побічних ефектів, оскільки надають нам інструменти для управління тим, які побічні ефекти можуть статися в даному фрагменті коду та коли.
Це виявляється, має дуже цікаві наслідки. По-перше, і, очевидно, є численні речі, які можна зробити за допомогою безкоштовного коду з побічними ефектами, які вже були описані. Але є й інші речі, які ми можемо зробити навіть, працюючи з кодом, який має побічні ефекти:
У складних кодових базах складні взаємодії побічних ефектів - це найскладніше, про що я можу пояснити. Я можу говорити лише особисто, враховуючи, як працює мій мозок. Побічні ефекти та стійкі стани, мутація входів і так далі змушують мене думати про те, "коли" і "де", що відбувається, щоб міркувати про правильність, а не лише про те, що відбувається в кожній окремій функції.
Я не можу просто зосередитись на "що". Я не можу зробити висновок після ретельного тестування функції, яка спричиняє побічні ефекти, що вона поширює ефір надійності по всьому коду, використовуючи її, оскільки абоненти можуть все-таки неправомірно використовувати її, називаючи її в неправильний час, з неправильної нитки, в неправильній замовлення. Тим часом функцію, яка не викликає побічних ефектів і просто повертає новий вихід із поданим входом (не торкаючись введення), в такий спосіб неправильно використати таким чином.
Але я прагматичний тип, я думаю, або, принаймні, намагаюся бути таким, і я не думаю, що нам обов'язково потрібно видаляти всі побічні ефекти до найменшого мінімуму, щоб міркувати про правильність нашого коду (принаймні Мені це було б важко зробити на таких мовах, як C). Де мені важко міркувати про правильність, коли у нас поєднання складних контрольних потоків і побічних ефектів.
Складні потоки управління до мене - це ті, що мають графічний характер, часто рекурсивний або рекурсивний (черги подій, наприклад, які не викликають безпосередньо події рекурсивно, але є "рекурсивно-подібними" в природі), можливо, роблячи щось в процесі проходження фактичної пов'язаної структури графіків або оброблення неоднорідної черги подій, яка містить еклектичну суміш подій для обробки, що веде нас до різного роду різних частин кодової бази і викликає різні побічні ефекти. Якби ви спробували намалювати всі місця, які в кінцевому підсумку ви опинитесь у коді, це буде нагадувати складний графік і, можливо, з вузлами в графіку, яких ви ніколи не очікували, були б там у той момент, і враховуючи, що вони всі викликаючи побічні ефекти,
Функціональні мови можуть мати надзвичайно складні та рекурсивні потоки управління, але результат настільки легко зрозуміти з точки зору коректності, оскільки в цьому процесі не відбувається всіляких еклектичних побічних ефектів. Лише тоді, коли складні контрольні потоки зустрічають еклектичні побічні ефекти, я вважаю, що це головний біль намагається зрозуміти всю суть того, що відбувається, і чи завжди це буде робити правильно.
Тому, коли у мене є такі випадки, мені часто буває дуже важко, якщо не неможливо, відчувати себе дуже впевнено щодо правильності такого коду, не кажучи вже про дуже впевнену, що я можу внести зміни до такого коду, не натрапивши на щось несподіване. Тож для мене рішення полягає в тому, щоб або спростити потік управління, або мінімізувати / уніфікувати побічні ефекти (маючи на увазі, я маю на увазі, як лише спричинення одного типу побічних ефектів для багатьох речей під час певної фази в системі, а не двох, трьох чи а десяток). Мені потрібно одна з цих двох речей, щоб мій простой мозок міг впевнено почувати правильність наявного коду та правильність змін, які я ввожу. Бути досить впевненим у правильності введення коду побічних ефектів, якщо побічні ефекти одноманітні та прості разом із контрольним потоком, наприклад:
for each pixel in an image:
make it red
Думати про правильність такого коду досить просто, але головним чином через те, що побічні ефекти настільки рівномірні, а контрольний потік настільки мертвий. Але скажімо, у нас був такий код:
for each vertex to remove in a mesh:
start removing vertex from connected edges():
start removing connected edges from connected faces():
rebuild connected faces excluding edges to remove():
if face has less than 3 edges:
remove face
remove edge
remove vertex
Тоді це смішно надпрощений псевдокод, який, як правило, передбачає набагато більше функцій та вкладених циклів та багато іншого, що потрібно буде продовжувати (оновлення декількох карт текстури, ваги кісток, станів вибору тощо), але навіть псевдокод робить це так важко причина правильності через взаємодію складного графічного потоку управління та побічних ефектів, що тривають. Отож одна стратегія спрощення - відкласти обробку та зосередитись на одному типі побічних ефектів за один раз:
for each vertex to remove:
mark connected edges
for each marked edge:
mark connected faces
for each marked face:
remove marked edges from face
if num_edges < 3:
remove face
for each marked edge:
remove edge
for each vertex to remove:
remove vertex
... щось в цьому плані як одна ітерація спрощення. Це означає, що ми передаємо дані декілька разів, що, безумовно, несе обчислювальні витрати, але ми часто виявляємо, що зможемо багатократно прочитати такий отриманий код легше, тепер, коли побічні ефекти та потоки управління набули такої рівномірної та простішої природи. Крім того, кожен цикл можна зробити більш сприятливим для кешу, ніж обхід підключеного графіка і спричинення побічних ефектів, коли ми йдемо (наприклад: використовувати паралельний набір бітів для позначення того, що потрібно пройти, щоб потім ми могли робити відкладені проходи в упорядкованому послідовному порядку з використанням бітових масок та FFS). Але найголовніше, що я вважаю другу версію набагато простішою міркуванням з точки зору правильності, а також зміни, не викликаючи помилок. Так що '
Зрештою, нам потрібні побічні ефекти в якийсь момент, інакше ми просто матимемо функції, які виводять дані нікуди. Часто нам потрібно щось записати у файл, щось вивести на екран, передати дані через сокет, щось подібне, і все це є побічними ефектами. Але ми, безумовно, можемо зменшити кількість зайвих побічних ефектів, які продовжуються, а також зменшити кількість побічних ефектів, що виникають, коли контрольні потоки дуже складні, і я думаю, що було б набагато простіше уникнути помилок, якби ми це зробили.
Це не зло. На мою думку, потрібно розрізняти два типи функцій - з побічними ефектами і без. Функція без побічних ефектів: - повертає завжди те саме з однаковими аргументами, тому, наприклад, така функція без жодних аргументів не має сенсу. - Це також означає, що порядок, в якому деякі такі функції називаються, не грає ніякої ролі - повинен бути в змозі запускатися і може бути налагоджений на самоті (!), Без іншого коду. А тепер, хай, подивися, що робить JUnit. Функція з побічними ефектами: - має свого роду "протікання", що можна виділити автоматично - це дуже важливо шляхом налагодження та пошуку помилок, що, як правило, викликано побічними ефектами. - Будь-яка функція з побічними ефектами також має "частину" без побічних ефектів, яку також можна відокремити автоматично. Тож злі ті побічні ефекти,