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