1. ホーム
  2. functional-programming

[解決済み】ミュータブルステートなしで何か役に立つことができるのか?

2022-03-31 08:30:43

質問

最近、関数型プログラミングについていろいろと読んでいて、だいたいは理解できるのですが、どうしても理解できないのがステートレス・コーディングです。 ミュータブルな状態を取り除いてプログラミングを単純化することは、ダッシュボードを取り除いて自動車を単純化するようなものです。

私が思いつく限りのユーザー・アプリケーションには、中核的な概念として状態が含まれています。 もしあなたが文書(あるいはSOポスト)を書けば、新しい入力があるたびに状態が変化します。 また、ビデオゲームをプレイする場合、常に動き回るキャラクタの位置を始めとして、大量の状態変数があります。 変化する値を追跡せずに、何か役に立つことができるでしょうか?

この問題を議論しているものを見つけると、いつも本当に技術的な関数語で書かれていて、私が持っていないFPの重い背景を想定しています。 どなたか、命令型コーディングはしっかり理解しているが、関数型は全く知らないという人にこの問題を説明する方法をご存知でしょうか?

EDIT: これまでの返信の多くは、不変の値の利点を納得させようとしているようです。 私はその部分を理解します。 それは完全に理にかなっています。 私が理解できないのは、ミュータブル変数なしで、変化しなければならない、そして絶えず変化する値をどうやって追跡できるかということです。

どのように解決するのか?

<ブロッククオート

あるいは、ビデオゲームをプレイする場合、そこには 大量の状態変数があります。 すべての人の位置が キャラクターは動き回ることが多いので 常に。どうすれば を記録しておかないと、何の役にも立たない。 値を変更することはできますか?

ご興味があれば これは はErlangを使ったゲームプログラミングを説明する一連の記事です。

おそらくこの回答は気に入らないでしょうが、あなたは 得る 機能的なプログラムは、使ってみないとわかりません。サンプルコードを載せて、こう言うこともできます。 見る でも、構文や基本原理を理解していないと、目が曇ってしまうんです。あなたから見ると、命令型言語と同じことをやっているのに、いろいろな境界線を設けて、わざとプログラミングを難しくしているように見えますね。私から見ると、あなたはただ単に ブルブパラドックス .

最初は半信半疑だったのですが、数年前に関数型プログラミングの列車に飛び乗り、その魅力に取り付かれました。関数型プログラミングのコツは、パターンや特定の変数の割り当てを認識し、命令的な状態をスタックに移動させることができることです。例えば、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);
        }
    }
}

上記のコードでは、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パーサーは は完全にステートレスで(少なくとも 私の のコードはステートレスで、基盤となるレキシングライブラリがステートレスかどうかはわかりません)。

ステートレス・プログラミングは、ステートフル・プログラミングと同様に表現力が豊かで強力です。ただ、ステートレス的に考えるようになるには、少し練習が必要です。もちろん、「可能な限りステートレスで、必要な場合はステートフルに」というのは、多くの不純な関数型言語のモットーのように思われます。関数型アプローチではクリーンで効率的でない場合、ミュータブルに戻るのは悪いことではありません。