Як можна зробити щось корисне без змінного стану?


265

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

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

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

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



1
Моя особиста скромна думка - це як сила і гроші. Діє закон зменшення прибутку. Якщо ви досить сильні, може бути мало стимулів стати трохи сильнішими, але працювати на це не завадить (а деякі люди роблять пристрасть). Це ж стосується глобального стану, що змінюється. Моє особисте вподобання сприймати, що в міру розвитку моєї навички кодування добре обмежити кількість глобального стану, що змінюється, в моєму коді. Це ніколи не може бути ідеальним, але добре працювати над мінімізацією глобального стану, що змінюється.
AturSams

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

7
Якщо коротко, FP, функції ніколи не змінюють стан. Згодом вони повернуть щось, що замінить поточний стан. Але стан ніколи не змінюється (мутується) на місці.
jinglesthula

Є способи отримати державність без мутацій (використовуючи стек з того, що я розумію), але це питання є в певному сенсі поруч із точкою (навіть якщо це чудово). Важко говорити про лаконічно, але ось пост, який, сподіваємось, відповідає на ваше питання medium.com/@jbmilgrom/… . TLDR полягає в тому, що семантика навіть стаціонарної функціональної програми є непорушною, однак обмін інформацією з розрахунку ж / б функцій програми обробляється.
jbmilgrom

Відповіді:


166

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

Якщо вам цікаво, ось низка статей, в яких описано ігрове програмування з Erlang.

Можливо, ця відповідь вам не сподобається, але ви не отримаєте функціональну програму, поки не використаєте її. Я можу розмістити зразки коду і сказати "Ось, ти не бачиш ", - але якщо ти не розумієш синтаксис і основні принципи, то твої очі просто засклиться. З вашої точки зору, схоже, я роблю те саме, що і імперативна мова, але просто встановлюю всілякі межі, щоб цілеспрямовано ускладнити програмування. Моя точка зору, ви просто переживаєте парадокс Blub .

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

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

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

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Метод Seq.iter буде перерахований через колекцію та викликає анонімну функцію для кожного елемента. Дуже зручно :)

Я знаю, кількість друку не дуже вражає. Однак ми можемо використовувати той самий підхід і з іграми: утримуйте весь стан у стеці та створюйте новий об'єкт з нашими змінами в рекурсивному виклику. Таким чином, кожен кадр являє собою знімок без громадянства гри, де кожен кадр просто створює абсолютно новий об'єкт з потрібними змінами будь-яких об'єктів без громадянства, необхідних для оновлення. Псевдокодом для цього може бути:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

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

Це масштабується до будь-якої кількості об'єктів у грі, тому що всі об'єкти (або колекції суміжних об’єктів) можуть бути відображені у власній темі.

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

У функціональних мовах, замість того, щоб мутувати стан об'єктів, ми просто повертаємо новий об’єкт із потрібними нам змінами. Його ефективніше, ніж це звучить. Наприклад, структури даних дуже легко представити як незмінні структури даних. Наприклад, стеки, як відомо, легко здійснити:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Код, описаний вище, будує два незмінні списки, додає їх разом для створення нового списку та додає результати. Ні в якому разі не змінюється стан у додатку. Це виглядає трохи об'ємно, але це лише тому, що C # є багатослівною мовою. Ось еквівалентна програма у F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Немає змінних, необхідних для створення та маніпулювання списками. Майже всі структури даних можуть бути легко перетворені у їхні функціональні еквіваленти. Я написав сторінку тут , яка забезпечує незмінні реалізацій стеків, черг, лівацьких купах, червоно-чорні дерева, ледачі списки. Жоден фрагмент коду не містить стану, що змінюється. Щоб "мутувати" дерево, я створюю нове з новим вузлом, який я хочу - це дуже ефективно, тому що мені не потрібно робити копію кожного вузла на дереві, я можу повторно використовувати старі в своєму новому дерево.

Використовуючи більш вагомий приклад, я також написав цей SQL-аналізатор, який абсолютно без стану (або принаймні мій код без стану, я не знаю, чи лежить в основі бібліотека лексингу без стану).

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


7
Мені подобається приклад Pacman. Але це може вирішити одну проблему лише для того, щоб підняти іншу: Що робити, якщо щось інше має посилання на існуючий об'єкт Pacman? Тоді він не буде зібраний і замінений сміттям; замість цього ви отримуєте дві копії об'єкта, одна з яких недійсна. Як ви вирішуєте цю проблему?
Мейсон Вілер

9
Очевидно, що вам потрібно створити нове "щось інше" з новим об'єктом Pacman;) Звичайно, якщо ми заведемо цей маршрут занадто далеко, ми в кінцевому підсумку відтворимо графік об'єктів для всього нашого світу щоразу, коли щось зміниться. Тут описаний кращий підхід ( prog21.dadgum.com/26.html ): замість того, щоб об’єкти оновлювали себе та всі свої залежності, набагато простіше, щоб вони передавали повідомлення про свій стан у цикл подій, який обробляє всі оновлення. Це набагато простіше визначити, які об’єкти в графіку потребують оновлення, а які - ні.
Джульєтта

6
@Juliet, у мене є одне сумніви - в моєму абсолютно імперативному режимі мислення рекурсія повинна закінчитися в якийсь момент, інакше ви в кінцевому підсумку призведе до переповнення стека. У рекурсивному прикладі Pacman, як стек зберігається в страху - чи об'єкт неявно спливає на початку функції?
BlueStrat

9
@BlueStrat - гарне запитання ... якщо це "хвостовий дзвінок" ... тобто рекурсивний виклик - це останнє в функції ... тоді системі не потрібно генерувати новий кадр стека ... він може просто повторно використовуйте попередній. Це загальна оптимізація функціональних мов програмування. en.wikipedia.org/wiki/Tail_call
ретептиліан

4
@MichaelOsofsky, коли взаємодіє з базами даних та API, завжди існує "зовнішній світ", з яким потрібно спілкуватися. У цьому випадку ви не можете на 100% функціонувати. Важливо тримати цей "нефункціональний" код ізольованим та абстрагованим, щоб було лише один запис та один вихід у зовнішній світ. Таким чином ви можете зберегти решту свого коду функціональним.
Кільте

76

Коротка відповідь: не можна.

То в чому тоді суєта з незмінністю?

Якщо ти добре розбираєшся в імперативній мові, то знаєш, що "глобали - це погано". Чому? Тому що вони вводять (або мають потенціал ввести) деякі дуже важко розв’язати залежності у вашому коді. І залежності - це не добре; ви хочете, щоб ваш код був модульним . Частини програми не впливають на інші частини якнайменше. І FP приводить вас до святого граалу модульності: побічних ефектів зовсім немає . Ви просто маєте свій f (x) = y. Покладіть x, вийдіть y. Ніяких змін до x чи нічого іншого. FP змушує вас перестати думати про стан і почати думати з точки зору цінностей. Усі ваші функції просто отримують значення та створюють нові значення.

Це має ряд переваг.

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

По-друге, це робить програму тривіально паралельною (ефективна паралелізація - інша справа).

По-третє, є деякі можливі переваги в роботі. Скажіть, у вас є функція:

double x = 2 * x

Тепер ви ставите значення 3 дюйма, і ви отримуєте значення 6. Кожного разу. Але ви можете це робити і в імперативі, правда? Так. Але проблема в тому, що в імперативі ви можете зробити ще більше . Я можу зробити:

int y = 2;
int double(x){ return x * y; }

але я також міг би зробити

int y = 2;
int double(x){ return x * (y++); }

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

Тепер, хоча створення нових значень кожного разу видається неймовірно марним для складних типів значень з точки зору пам'яті комп'ютера, це не повинно бути таким. Тому що, якщо у вас f (x) = y, а значення x і y "здебільшого однакові" (наприклад, дерева, які відрізняються лише кількома листями), то x і y можуть ділитися частинами пам'яті - тому що жодне з них не буде мутувати .

Тож якщо ця незмінна річ настільки велика, чому я відповів, що нічого не корисного не можна зробити без змінного стану. Ну, без змін, вся ваша програма була б функцією гігантського f (x) = y. І те саме стосується всіх частин вашої програми: лише функцій, і функцій у "чистому" сенсі. Як я вже говорив, це означає, що кожного разу f (x) = y . Так, наприклад, readFile ("myFile.txt") повинен кожного разу повертати одне і те ж значення рядка. Не надто корисний.

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

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


2
<< readFile ("myFile.txt") повинен буде щоразу повертати одне і те ж значення рядка. Не надто корисно. >> Я здогадуюсь, що це корисно, якщо ви ховаєте глобальну, файлову систему. Якщо ви розглядаєте це як другий параметр і дозволите іншим процесам повертати нове посилання на файлову систему кожного разу, коли вони змінюють її файловою системою2 = write (fileystem1, fd, pos, "string"), і нехай усі процеси обмінюються посиланням на файлову систему , ми могли отримати набагато чіткішу картину операційної системи.
вугор ghEEz

@eelghEEz, це той самий підхід, який Datomic застосовує до баз даних.
Джейсон

1
+1 для чіткого та стислого порівняння парадигм. Одне з пропозицій, int double(x){ return x * (++y); }оскільки поточне все ще буде 4, хоча все ще має неорекламований побічний ефект, тоді як ++yповернеться 6.
BrainFRZ

@eelghEEz Я не впевнений в альтернативності, справді, хтось ще? Щоб ввести інформацію в контекст (чисто) ПС, ви "проводите вимірювання", наприклад "в часовій марці X, температура - Y". Якщо хтось запитує про температуру, він може неявно означати X = зараз, але вони, можливо, не можуть просити температуру як універсальну функцію часу, правда? FP має справу з незмінним станом, і ви повинні створити незмінний стан - із внутрішніх та зовнішніх джерел - із змінних. Індекси, часові позначки тощо є корисними, але ортогональними щодо змінності - як VCS - це сам контроль версій.
Джон П

29

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

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

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

Додаток: (Що стосується вашої редакції, як слідкувати за значеннями, які потрібно змінити)
Вони будуть зберігатися в незмінній структурі даних, звичайно ...

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

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


2
Гаразд, я змінив назву. Здається, ваша відповідь призводить до ще гіршої проблеми. Якщо мені доведеться відтворювати кожен об’єкт щоразу, коли щось змінюється в його стані, я витрачу весь свій процесорний час, не роблячи нічого, крім конструювання об'єктів. Я думаю про ігрове програмування тут, де у вас є багато речей, що рухаються по екрану (і поза екраном) відразу, що потрібно мати можливість взаємодіяти один з одним. Весь двигун має встановлений кадр: все, що ви збираєтеся робити, ви повинні зробити за X кількість мілісекунд. Звичайно, є кращий спосіб, ніж постійно переробляти цілі об'єкти?
Мейсон Уілер

4
Краса в тому, що імітація полягає в мові, а не в реалізації. За допомогою декількох хитрощів ви можете мати незмінний стан мови, тоді як реалізація фактично змінює стан на місці. Дивіться, наприклад, ST монади Haskell.
CesarB

4
@Mason: Справа в тому, що компілятор може набагато краще вирішити, де безпечно змінити стан на місці, ніж ви можете.
jerryjvl

Я думаю, що для ігор вам слід уникати незмінних для будь-яких частин, де швидкість не має значення. Хоча незмінна мова може оптимізувати для вас, нічого не буде швидшим, ніж зміна пам’яті, яка швидко працює на процесорах. І тому, якщо виявляється, є 10 або 20 місць, де вам потрібно вкрай необхідне, я думаю, вам слід просто взагалі уникати незмінних випадків, якщо ви не зможете модулювати його для дуже відокремлених областей, таких як ігрові меню. І саме логічна гра може бути гарним місцем для використання непорушних, тому що я вважаю, що це чудово для того, щоб робити складне моделювання чистих систем, таких як бізнес-правила.
LegendLength

@LegendLength ти суперечить собі.
Ixx

18

Я думаю, є невелике непорозуміння. Чисті функціональні програми мають державні. Різниця полягає в тому, як моделюється цей стан. У чистому функціональному програмуванні стан маніпулює функціями, які приймають певний стан і повертають наступний стан. Потім послідовності через стани досягаються шляхом передачі стану через послідовність чистих функцій.

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

Ви хотіли, щоб це було пояснено з точки зору імперативного програмування. Добре, давайте розглянемо кілька дійсно простих імперативних програмувань на функціональній мові.

Розглянемо цей код:

int x = 1;
int y = x + 1;
x = x + y;
return x;

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

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Поставте дужки, щоб зрозуміти, що це означає:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

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

Ви побачите, що ця модель може моделювати будь-який стан, навіть IO.


Це такий, як Монада?
CMCDragonkai

Чи вважаєте ви це: A декларативним на рівні 1 B є декларативним на рівні 2, він вважає A імперативом. C є декларативним на рівні 3, він вважає B імперативом. Коли ми збільшуємо рівень абстракції, він завжди вважає, що мови, розташовані нижче на шарі абстракції, є більш імперативними, ніж сама.
CMCDragonkai

14

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

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

стає цим функціональним кодом (синтаксис схожий на схему):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

або цей код Haskellish

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

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

Ви можете знайти хороший підручник з великою кількістю прикладів у статті Джона Х'юза Чому питання функціонального програмування мають значення .


13

Це просто різні способи зробити те саме.

Розглянемо простий приклад, такий як додавання чисел 3, 5 та 10. Уявіть собі, як це зробити, спочатку змінивши значення 3, додавши до нього 5, потім додавши 10 до цього "3", а потім виведете поточне значення " 3 "(18). Це здається явно смішним, але це, по суті, те, як часто робиться імперативне програмування, засноване на державі. Дійсно, у вас може бути багато різних "3", які мають значення 3, але є різними. Все це здається дивним, оскільки ми були настільки вкорінені в цілком надзвичайно розумну думку, що цифри незмінні.

Тепер подумайте про додавання 3, 5 і 10, коли будете приймати значення незмінні. Ви додаєте 3 і 5, щоб отримати ще одне значення, 8, потім додаєте 10 до цього значення, щоб отримати ще одне значення, 18.

Це рівнозначні способи зробити те саме. Вся необхідна інформація існує в обох методах, але в різних формах. В одному інформація існує як державна, так і в правилах зміни стану. В іншому інформація існує у незмінних даних та функціональних визначеннях.


10

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

  1. Функціональні мови підтримують точно такі ж оновлення стану, що й імперативні мови, але вони роблять це, передаючи оновлений стан наступним викликам функцій . Ось дуже простий приклад руху вниз по рядку чисел. Ваш стан - це ваше поточне місцезнаходження.

Спочатку необхідний спосіб (у псевдокоді)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

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

predicate ? if-true-expression : if-false-expression

Ви можете пов'язати потрійний вираз, поставивши на місце хибного виразу новий потрійний вираз

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Отже, маючи на увазі, ось функціональна версія.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

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

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

  2. Навчитися мислити рекурсивно не важко, але це вимагає як практики, так і інструментарію. Цей невеликий розділ у тій книзі "Дізнайся Java", де вони використовували рекурсію для обчислення факторіалу, не скорочує її. Вам потрібен набір інструментів, таких як створення ітеративних процесів із рекурсії (саме тому рекурсія хвоста є важливою для функціональної мови), продовження, інваріанти тощо. Ви б не займалися програмуванням OO, не дізнавшись про модифікатори доступу, інтерфейси тощо. Те саме для функціонального програмування.

Моя рекомендація - це зробити Маленький Схемер (зауважте, що я кажу "робіть", а не "читайте"), а потім виконувати всі вправи в SICP. Коли ви закінчите, у вас буде інший мозок, ніж коли ви починали.


8

Насправді досить легко створити щось, що виглядає як стан, що змінюється, навіть у мовах без змінних.

Розглянемо функцію з типом s -> (a, s). Перекладаючи з синтаксису Haskell, він означає функцію, яка приймає один параметр типу " s" і повертає пари значень типів " a" і " s". Якщо sце тип нашої держави, ця функція приймає один стан і повертає новий стан, і, можливо, значення (ви завжди можете повернути "одиницю" ака (), яка на зразок еквівалентна "void " в C / C ++, як " a" тип). Якщо ви пов’язуєте кілька викликів функцій таких типів (повернення стану з однієї функції та перехід її до наступної), у вас є стан "мутація" (адже ви в кожній функції створюєте новий стан та відмовляєтесь від старого ).

Це може бути простіше зрозуміти, якщо ви уявляєте стан, що змінюється, як "простір", де виконується ваша програма, а потім подумайте про часовий вимір. У момент t1 "простір" знаходиться в певному стані (скажімо, наприклад, деяке місце в пам'яті має значення 5). З пізнішим миттю t2 він перебуває в іншому стані (наприклад, місце розташування пам'яті зараз має значення 10). Кожен з цих "часових фрагментів" є станом, і він незмінний (ви не можете повернутися в часі, щоб змінити їх). Отже, з цієї точки зору ви перейшли від повного простору із стрілкою часу (стан, що змінюється) до набору фрагментів простору часу (декілька незмінних станів), і ваша програма просто розглядає кожен фрагмент як значення та обчислює кожен з них як функція, застосована до попередньої.

Добре, можливо, це було не простіше зрозуміти :-)

Можливо, здасться неефективним явно представляти весь стан програми як значення, яке має бути створене лише для того, щоб відкинути наступного моменту (одразу після створення нового). Для деяких алгоритмів це може бути природним, але коли його немає, є ще одна хитрість. Замість реального стану ви можете використовувати підроблений стан, який є не що інше, як маркер (назвемо тип цього підробленого стануState# ). Цей підроблений стан існує з точки зору мови і передається, як і будь-яке інше значення, але компілятор повністю його опускає при генерації машинного коду. Він служить лише для позначення послідовності виконання.

Наприклад, припустимо, що компілятор надає нам такі функції:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

Перекладаючи з цих оголошень, подібних до Haskell, readRefотримує щось, що нагадує вказівник або ручку на значення типу " a" і фальшивий стан, і повертає значення типу " a", на яке вказує перший параметр і новий підроблений стан.writeRefподібний, але змінює значення, вказане натомість.

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

(Ті , хто знає Haskell помітить я спростив речі багато і кілька важливих опущений деталей. Для тих , хто хоче бачити більше деталей, подивіться на Control.Monad.Stateвід mtl, так і на ST sі IO(AKA ST RealWorld) монад.)

Вам може бути цікаво, чому це робити таким круговим способом (замість того, щоб просто міняти стан мови). Справжня перевага полягає в тому, що ви змінили стан своєї програми. Те, що раніше було неявним (стан вашої програми був глобальним, дозволяючи робити такі дії, як дії на відстані ), тепер явно. Функції, які не отримують і не повертають стан, не можуть змінювати його або впливати на нього; вони "чисті". Ще краще, ви можете мати окремі потоки стану, і за допомогою трохи магічного типу вони можуть бути використані для вбудовування імперативного обчислення в чистому, не роблячи його нечистим ( STмонада в Haskell - це та, яка зазвичай використовується для цього трюку; State#я вже згадував вище, в тому , GHC, State# s, використовується його реалізації монади).ST іIO


7

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


Коли я був більш функціональним, я замислювався над тим, як це може бути добре, коли щось на зразок жорсткого диска є змінним. Усі мої класи c # мали стан, що змінюється, і могли дуже логічно імітувати жорсткий диск або будь-який інший пристрій. Тоді як з функціоналом було невідповідність між моделями та власне машинами, які вони моделювали. Заглибившись у функціонал, я зрозумів, що ви можете отримати переваги, які ви отримаєте. І якби фізично можна було винайти жорсткий диск, який зробив його копію, це насправді було б корисно (як, наприклад, поїздки вже є).
LegendLength

5

Окрім чудових відповідей, які дають інші, подумайте про заняття Integerта Stringна Java. Примірники цих класів незмінні, але це не робить класи марними лише тому, що їх екземпляри неможливо змінити. Незмінність дає певну безпеку. Ви знаєте, що якщо ви використовуєте екземпляр String або Integer як ключ до a Map, ключ не можна змінити. Порівняйте це з Dateкласом на Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Ви мовчки змінили ключ на своїй карті! Робота з незмінними об'єктами, як, наприклад, у функціональному програмуванні, набагато чистіша. Простіше міркувати про те, які побічні ефекти трапляються - жодні! Це означає, що програмісту простіше, а також оптимізатору простіше.


2
Я розумію це, але це не відповідає на моє запитання. Маючи на увазі, що комп'ютерна програма - це модель якоїсь реальної події чи процесу, якщо ви не можете змінити свої цінності, то як ви змоделюєте щось, що змінюється?
Мейсон Уілер

Ну, ви, звичайно, можете робити корисні речі з класами Integer і String. Це не так, як їх незмінність означає, що ви не можете мати стан, що змінюється.
Едді

@ Мейсон Уілер - розуміючи, що річ і її стан - це дві різні "речі". Що Pacman - це не змінюється від часу A до B. Там, де Pacman, змінюється. Коли ви переходите від часу А до часу В, ви отримуєте нове поєднання pacman + state ... яке є тим самим pacman, різним станом. Не змінено стан ... інший стан.
RHSeeger

4

Для інтерактивних додатків, таких як ігри, функціональне реактивне програмування - ваш друг: якщо ви можете сформулювати властивості світу вашої гри як значення, що змінюються часом (та / або потоки подій), ви готові! Ці формули будуть іноді навіть більш природними та розкритими у намірах, ніж мутація стану, наприклад, для рухомої кулі можна безпосередньо використовувати відомий закон x = v * t . І що краще, правила гри, написані таким чином, складають краще, ніж об'єктно-орієнтовані абстракції. Наприклад, у цьому випадку швидкість руху кулі може бути також значенням, що змінюється у часі, що залежить від потоку подій, що складається із зіткнень м'яча. Більш конкретні міркування щодо дизайну див. У розділі Створення ігор у в'язах .


4

3

Таким чином FORTRAN буде працювати без загальних блоків: Ви б писали методи, у яких були значення, які ви передавали, та локальні змінні. Це воно.

Об'єктно-орієнтоване програмування поєднало нас у стані та поведінці, але це була нова ідея, коли я вперше зіткнувся з цим у C ++ ще в 1994 році.

Боже, я був функціональним програмістом, коли я був інженером-механіком, і цього не знав!


2
Я не погоджуюся, що це щось, що можна зафіксувати на ОО. Мови перед OO заохочували стан зв'язку та алгоритми. OO просто запропонував кращий спосіб управління ним.
Джейсон Бейкер

"Підбадьорений" - можливо. OO роблять це явною частиною мови. Ви можете робити інкапсуляцію та інформацію, ховаючись у C, але я б сказав, що мови OO значно спрощують її.
duffymo

2

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


Тільки тому, що дві мови є Тьюрінгом завершеними, це не означає, що вони можуть виконувати однакові завдання. Це означає, що вони можуть проводити ті ж обчислення. Brainfuck Turing завершений, але я впевнений, що він не може спілкуватися через TCP-стек.
RHSeeger

2
Звичайно, це може. Враховуючи той самий доступ до апаратних засобів, як і C, він може. Це не означає, що це було б практично, але можливість є.
Джейсон Бейкер

2

Ви не можете мати корисну чисту функціональну мову. Завжди буде рівень змінності, з яким вам доведеться мати справу, IO - один із прикладів.

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



-3

Це дуже просто. У функціональному програмуванні ви можете використовувати стільки змінних, але тільки в тому випадку, якщо вони є локальними змінними (містяться всередині функцій). Тож просто загорніть свій код у функції, передайте значення вперед і назад серед цих функцій (як передані параметри та повернені значення) ... і це все, що в ньому є!

Ось приклад:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

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