Або якщо ви граєте у відеоігри, є багато змінних стану, починаючи з позицій усіх персонажів, які, як правило, постійно рухаються. Як ви можете зробити щось корисне, не відстежуючи зміни значень?
Якщо вам цікаво, ось низка статей, в яких описано ігрове програмування з 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-аналізатор, який абсолютно без стану (або принаймні мій код без стану, я не знаю, чи лежить в основі бібліотека лексингу без стану).
Програмування без громадянства настільки ж виразне і потужне, як і програмування на державі, воно просто вимагає трохи практики, щоб навчити себе починати думати без громадянства. Звичайно, "програмування без громадянства, коли це можливо, державне програмування, де це необхідно", здається, девізом більшості нечистих функціональних мов. Немає шкоди відмовлятися від мутантів, коли функціональний підхід просто не такий чистий чи ефективний.