または、ビデオゲームをプレイする場合、すべてのキャラクターの位置をはじめ、絶えず動き回る傾向のある状態変数がたくさんあります。値の変化を追跡せずに、どうすれば便利なことをできるでしょうか?
興味がある場合は、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);
}
}
}
上記のコードは、2つの不変リストを作成し、それらを一緒に追加して新しいリストを作成し、結果を追加します。アプリケーションのどこでも変更可能な状態は使用されません。少しかさばるように見えますが、それは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パーサーも作成しました(または少なくとも私のコードはステートレスであり、基盤となる字句解析ライブラリがステートレスかどうかはわかりません)。
ステートレスプログラミングは、ステートフルプログラミングと同じように表現力があり強力ですが、ステートレスで考え始めるためのトレーニングを行うには少し練習が必要です。もちろん、「可能な場合はステートレスプログラミング、必要な場合はステートフルプログラミング」が、不純な関数型言語のモットーのようです。関数型のアプローチがクリーンでも効率的でもない場合でも、ミュータブルに頼っても害はありません。