또는 비디오 게임을하는 경우 모든 캐릭터의 위치부터 끊임없이 움직이는 경향이있는 수많은 상태 변수가 있습니다. 값 변경을 추적하지 않고 어떻게 유용한 것을 할 수 있습니까?
당신이 관심이 있다면, 여기 얼랑와 게임 프로그래밍을 설명하는 일련의 기사는.
이 답변이 마음에 들지 않지만 사용할 때까지 기능적 프로그램을 얻지 못할 것입니다. 나는 코드 샘플을 게시하고 말 "여기,하지 작업을 수행 할 수 있습니다 참조 "-하지만 당신은 구문과 기본 원리를 이해하지 않으면, 당신의 눈은 유약. 당신의 관점에서 볼 때, 나는 명령형 언어와 같은 일을하는 것처럼 보이지만 의도적으로 프로그래밍을 더 어렵게 만들기 위해 모든 종류의 경계를 설정합니다. 내 관점에서, 당신은 Blub 역설을 경험하고 있습니다.
처음에는 회의적이지만 몇 년 전에 기능 프로그래밍 기차를 타고 뛰어 들었습니다. 함수형 프로그래밍의 요령은 패턴, 특정 변수 할당을 인식하고 명령 상태를 스택으로 옮길 수 있습니다. 예를 들어 for-loop는 재귀가됩니다.
// 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 파서 를 작성 했습니다 (또는 적어도 내 코드는 상태가 없습니다. 기본 lexing 라이브러리가 상태가 없는지 모르겠습니다).
상태 비 저장 프로그래밍은 상태 비 저장 프로그래밍과 마찬가지로 표현력이 뛰어나고 강력하므로 무 상태로 사고를 시작하도록 약간의 연습 만하면됩니다. 물론 "가능한 경우 상태 비 저장 프로그래밍, 필요한 경우 상태 저장 프로그래밍"은 가장 불완전한 기능 언어의 모토로 보입니다. 기능적 접근 방식이 깨끗하지 않거나 효율적이지 않을 때 변경 가능 변수로 넘어가는 데 아무런 해가 없습니다.