Для цілей цієї відповіді я визначаю "суто функціональна мова", щоб означати функціональну мову, в якій функції референтно прозорі, тобто виклик однієї і тієї ж функції кілька разів з одними і тими ж аргументами завжди дасть однакові результати. Це, я вважаю, звичайне визначення суто функціональної мови.
Чисті функціональні мови програмування не допускають побічних ефектів (і тому мало корисні на практиці, оскільки будь-яка корисна програма має побічні ефекти, наприклад, коли вона взаємодіє із зовнішнім світом).
Найпростішим способом досягти референтної прозорості було б дійсно заборонити побічні ефекти, і справді існують мови, в яких це відбувається (здебільшого конкретні доменні). Однак це, безумовно, не єдиний спосіб, а найбільш функціональні мови загальної цілі (Haskell, Clean, ...) дозволяють побічний ефект.
Крім того, кажучи, що мова програмування без побічних ефектів мало корисна на практиці, я не думаю, що, звичайно, не для доменних мов, але навіть для мов загального призначення, я думаю, що мова може бути дуже корисною, не надаючи побічних ефектів . Можливо, не для консольних додатків, але я думаю, що програми GUI можна добре реалізувати без побічних ефектів у, скажімо, функціональній реактивній парадигмі.
Щодо пункту 1, ви можете взаємодіяти із середовищем на чисто функціональних мовах, але вам потрібно чітко позначити код (функції), який їх вводить (наприклад, у Haskell за допомогою монадичних типів).
Це трохи над спрощенням. Просто існування системи, де побічні функції потрібно позначати як такі (подібні до правильності const у C ++, але із загальними побічними ефектами), недостатньо для забезпечення еталонної прозорості. Вам потрібно переконатися, що програма ніколи не може викликати функцію кілька разів з однаковими аргументами та отримувати різні результати. Ви можете це зробити, зробивши подібні речіreadLine
бути чимось, що не є функцією (саме це робить Haskell з монадою IO), або ви могли б унеможливити багаторазовий виклик побічних функцій одним і тим же аргументом (це робить Clean). В останньому випадку компілятор гарантує, що кожного разу, коли ви викликаєте побічну функцію, ви робите це з новим аргументом, і він буде відхиляти будь-яку програму, де ви передаєте один і той же аргумент сторонній функції.
Чисті функціональні мови програмування не дозволяють писати програму, яка підтримує стан (що робить програмування дуже незручним, оскільки в багатьох додатках вам потрібна держава).
Знову ж таки, що суто функціональна мова може дуже добре забороняти стан, що змінюється, але, безумовно, можна бути чистим і все-таки мати стан, що змінюється, якщо реалізувати його так само, як я описав із побічними ефектами вище. Дійсно мінливий стан - це ще одна форма побічних ефектів.
Однак, функціональні мови програмування, безумовно, перешкоджають зміненному стану - особливо таким. І я не думаю, що це робить програмування незручним - зовсім навпаки. Іноді (але не все, що часто) змінне стан неможливо уникнути без втрати продуктивності чи ясності (саме тому такі мови, як Haskell, мають засоби для змінного стану), але найчастіше це можна.
Якщо вони є хибними уявленнями, як вони виникли?
Я думаю, що багато людей просто читають "функція повинна давати той самий результат, коли викликається з однаковими аргументами", і роблять висновок, що реалізувати щось на кшталт readLine
або код, який підтримує стан, що змінюється, неможливо . Тож вони просто не знають про "чити", які суто функціональні мови можуть використовувати для впровадження цих речей, не порушуючи еталонної прозорості.
Також стан, що змінюється, сильно перешкоджає функціональним мовам, тому не так вже й великий стрибок, щоб припустити, що він не дозволений у чисто функціональних мовах.
Чи можете ви написати (можливо, невеликий) фрагмент коду, що ілюструє ідіоматичний спосіб Haskell (1) реалізувати побічні ефекти та (2) здійснити обчислення зі станом?
Ось додаток у Псевдо-Хаскелл, який запитує у користувача ім’я та вітає його. Псевдо-Haskell - це мова, яку я щойно винайшов, і має систему вводу-виводу Haskell, але використовує більш звичайний синтаксис, більш описові назви функцій і не має do
примітки (оскільки це просто відволікатиметься від того, як саме працює монада IO):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
Підказкою тут є те, що readLine
це значення типу IO<String>
і composeMonad
це функція, яка приймає аргумент типу IO<T>
(для певного типу T
) та інший аргумент, який є функцією, яка приймає аргумент типу T
і повертає значення типу IO<U>
(для певного типу U
). print
це функція, яка приймає рядок і повертає значення типу IO<void>
.
Значення типу IO<A>
- це значення, яке "кодує" дану дію, яка виробляє значення типу A
. composeMonad(m, f)
створює нове IO
значення, яке кодує дію, за m
якою слідує дія f(x)
, де x
значення виробляє, виконуючи дію m
.
Стан, що змінюється, виглядатиме так:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Ось mutableVariable
функція, яка приймає значення будь-якого типу T
і виробляє a MutableVariable<T>
. Функція getValue
приймає MutableVariable
та повертає значення, IO<T>
яке виробляє його поточне значення. setValue
приймає a MutableVariable<T>
і a T
і повертає значення, IO<void>
яке встановлює значення. composeVoidMonad
те саме, що, composeMonad
за винятком того, що перший аргумент - це IO
той, що не дає значущого значення, а другий аргумент - інша монада, а не функція, яка повертає монаду.
У Haskell є якийсь синтаксичний цукор, який робить це ціле випробування менш болісним, але все одно очевидно, що стан, що змінюється, - це те, що мова насправді не хоче, щоб ти робив.