Или, если вы играете в видеоигру, существует множество переменных состояния, начиная с позиций всех персонажей, которые имеют тенденцию постоянно перемещаться. Как вы можете сделать что-нибудь полезное, не отслеживая изменения значений?
Если вам интересно, вот серия статей, которые описывают программирование игр с Erlang.
Вам, вероятно, не понравится этот ответ, но вы не получите функциональную программу, пока не воспользуетесь ею. Я могу опубликовать примеры кода и сказать: «Вот, разве вы не видите », но если вы не понимаете синтаксис и основополагающие принципы, тогда ваши глаза просто глазурят. С вашей точки зрения, похоже, что я делаю то же самое, что и императивный язык, но просто устанавливаю все виды границ, чтобы целенаправленно усложнять программирование. Моя точка зрения, вы просто испытываете парадокс Блюба .
Сначала я был настроен скептически, но несколько лет назад я запрыгнул на поезд по функциональному программированию и влюбился в него. Уловка с функциональным программированием заключается в способности распознавать шаблоны, конкретные назначения переменных и перемещать императивное состояние в стек. Например, цикл 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, который полностью не имеет состояния (или, по крайней мере, мой код не имеет состояния, я не знаю, является ли лежащая в основе библиотека лексирования без сохранения состояния).
Программирование без учета состояния столь же выразительно и мощно, как и программирование с состоянием, просто требуется небольшая практика, чтобы научиться думать без состояния. Конечно, «программирование без сохранения состояния, когда это возможно, программирование с сохранением состояния, где это необходимо» - это девиз большинства нечистых функциональных языков. Нет ничего плохого в том, чтобы вернуться к изменчивым файлам, когда функциональный подход не так чист и эффективен.