変更可能な状態なしでどのようにして有用なことを実行できますか


265

私は最近、関数型プログラミングについて多くのことを読んでいますが、そのほとんどは理解できますが、頭に入れられないのは、ステートレスコーディングです。ミュータブルな状態を削除してプログラミングを単純化することは、ダッシュボードを削除して自動車を「単純化」するようなものだと思います。

私が考えることができるほぼすべてのユーザーアプリケーションは、コアコンセプトとして状態を含みます。ドキュメント(またはSOポスト)を作成すると、状態は新しい入力ごとに変化します。または、ビデオゲームをプレイする場合、すべてのキャラクターの位置をはじめ、絶えず動き回る傾向のある状態変数がたくさんあります。値の変化を追跡せずに、どうすれば便利なことをできるでしょうか?

この問題について説明しているものを見つけるたびに、それは私が持っていないFPのバックグラウンドが重いと想定して、本当にテクニカルなファンクショナルで書かれています。命令型コーディングについて十分に理解しているが、機能面で完全なn00bである人にこれを説明する方法を知っている人はいますか?

編集:これまでの返信の多くは、不変値の利点を私に納得させようとしているようです。私はその部分を手に入れました。それは完全に理にかなっています。私が理解していないのは、変更可能な変数を使用せずに、変更する必要がある値を常に追跡する方法です。



1
私の個人的な見解では、それは強さとお金のようなものです。収益の減少の法則が適用されます。あなたがかなり強い場合、少し強くなるインセンティブはほとんどないかもしれませんが、それで働くことは害にはなりません(そして、一部の人々は情熱を持ってやっています)。同じことがグローバルな変更可能な状態にも当てはまります。私の個人的な好みは、私のコーディングスキルが進歩するにつれて、コード内のグローバルな変更可能な状態の量を制限するのが良いことを受け入れることです。完璧になることは決してないかもしれませんが、グローバルな変更可能な状態を最小化することに取り組むのは良いことです。
AturSams 2017

お金と同様に、より多くの時間を投資することで到達するポイントは、もはや非常に有用ではなくなり、他の優先順位がトップに上がります。たとえば、あなたが可能な限り最大の強さに達した場合(私の比喩によると)、それは有用な目的を果たさない可能性があり、負担になる可能性さえあります。しかし、達成できない可能性のあるその目標に向けて努力し、そこに適度なリソースを投資することは依然として良いことです。
AturSams 2017

7
簡単に言うと、FPでは、関数が状態を変更することはありません。最終的には、現在の状態を置き換えるものを返します。ただし、状態がインプレースで変更(変更)されることはありません。
jinglesthula

変更なしでステートフル性を取得する方法はありますが(私が理解しているものからのスタックを使用して)、この質問はある意味ではありません(素晴らしい質問ですが)。簡潔に話すのは難しいですが、これは質問medium.com/@jbmilgrom/…への回答を期待しています。TLDRは、ステートフルな関数型プログラムのセマンティクスも不変ですが、プログラム関数のb / w通信の実行は処理されます。
jbmilgrom

回答:


166

または、ビデオゲームをプレイする場合、すべてのキャラクターの位置をはじめ、絶えず動き回る傾向のある状態変数がたくさんあります。値の変化を追跡せずに、どうすれば便利なことをできるでしょうか?

興味がある場合は、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パーサーも作成しました(または少なくとも私のコードはステートレスであり、基盤となる字句解析ライブラリがステートレスかどうかはわかりません)。

ステートレスプログラミングは、ステートフルプログラミングと同じように表現力があり強力ですが、ステートレスで考え始めるためのトレーニングを行うには少し練習が必要です。もちろん、「可能な場合はステートレスプログラミング、必要な場合はステートフルプログラミング」が、不純な関数型言語のモットーのようです。関数型のアプローチがクリーンでも効率的でもない場合でも、ミュータブルに頼っても害はありません。


7
私はパックマンの例が好きです。しかし、それは別の問題を提起するためだけに1つの問題を解決する可能性があります。他の何かが既存のパックマンオブジェクトへの参照を保持している場合はどうでしょうか?そうすれば、ガベージコレクションや置き換えは行われません。代わりに、オブジェクトの2つのコピーが作成され、そのうちの1つは無効です。この問題をどのように処理しますか?
メイソンウィーラー

9
もちろん、新しいPacmanオブジェクトで新しい「何か」を作成する必要があります;)もちろん、そのルートを行き過ぎると、何かが変更されるたびに全世界のオブジェクトグラフが再作成されます。ここでより良いアプローチを説明します(prog21.dadgum.com/26.html):オブジェクト自体とそのすべての依存関係を更新するよりも、すべてのイベントを処理するイベントループに状態に関するメッセージを渡す方がはるかに簡単です更新しています。これにより、グラフ内の更新が必要なオブジェクトと不要なオブジェクトを簡単に決定できます。
ジュリエット

6
@ジュリエット、私は1つの疑いを持っています-私の完全に強制的な考え方では、再帰はある時点で終了する必要があります。そうしないと、最終的にスタックオーバーフローが発生します。再帰的なパックマンの例では、スタックはどのようにしてベイに保持されますか?オブジェクトは関数の先頭で暗黙的にポップされますか?
BlueStrat 2015

9
@BlueStrat-良い質問...それが「末尾呼び出し」の場合...つまり、再帰呼び出しは関数の最後のものです...システムは新しいスタックフレームを生成する必要はありません...それはできます以前のものを再利用してください。これは、関数型プログラミング言語の一般的な最適化です。en.wikipedia.org/wiki/Tail_call
reteptilian

4
@MichaelOsofsky、データベースやAPIとやり取りするとき、通信する状態を持つ「外の世界」が常にあります。この場合、100%機能することはできません。この「機能しない」コードを分離して抽象化し、外界への入り口と出口が1つだけになるようにすることが重要です。このようにして、残りのコードを機能させることができます。
Chielt

76

短い答え:できません。

それでは、不変性についての大騒ぎは何ですか?

命令型言語に精通している場合は、「グローバルは悪い」ことを知っています。どうして?彼らはあなたのコードにいくつかの非常に難しい絡み合い依存関係を導入する(または導入する可能性がある)ためです。そして依存関係は良くありません。コードをモジュール化する必要がある。プログラムの一部は、他の部分にできるだけ影響を与えません。そして、FPはモジュール化の神聖な目標をもたらします:副作用はまったくありません。あなたはちょうどあなたのf(x)= yを持っています。xを入れ、yを取り出します。xやその他の変更はありません。FPを使用すると、状態について考えるのをやめ、値の観点から考えるようになります。すべての関数は単に値を受け取り、新しい値を生成します。

これにはいくつかの利点があります。

まず、副作用がないということは、プログラムが単純であり、推論が容易であることを意味します。プログラムの新しい部分を導入しても、既存の機能している部分が妨害されてクラッシュする心配はありません。

第二に、これはプログラムを簡単に並列化できるようにします(効率的な並列化は別の問題です)。

第三に、いくつかの可能なパフォーマンス上の利点があります。あなたが機能を持っているとしましょう:

double x = 2 * x

ここで、3の値を入力し、6の値を取得します。毎回。しかし、あなたはそれを命令型でも行うことができますよね?うん。しかし問題は、命令で、あなたはさらにもっとできるということです。できます:

int y = 2;
int double(x){ return x * y; }

でもできた

int y = 2;
int double(x){ return x * (y++); }

命令型コンパイラは、副作用があるかどうかを認識していないため、最適化がより困難になります(つまり、double 2が毎回4である必要はありません)。機能的なものは私が知らないことを知っています-したがって、「double 2」を見るたびに最適化できます。

今では、毎回新しい値を作成することは、コンピューターのメモリの観点から複雑なタイプの値にとって信じられないほど無駄に思えますが、そうである必要はありません。なぜなら、f(x)= yがあり、値xとyが「ほとんど同じ」(たとえば、いくつかの葉だけが異なる木)である場合、xとyはメモリの一部を共有できます-どちらも変化しないため。

したがって、この変更不可能なことが非常に優れている場合、変更可能な状態がないと、何も役に立たないというのはなぜでしょうか。まあ、可変性がないと、プログラム全体は巨大なf(x)= y関数になります。そして、同じことがプログラムのすべての部分に当てはまります。関数だけであり、「純粋な」意味での関数です。私が言ったように、これは毎回 f(x)= yを意味します。したがって、たとえばreadFile( "myFile.txt")は毎回同じ文字列値を返す必要があります。あまり役に立ちません。

したがって、すべてのFPは状態を変更するいくつかの手段を提供します。「純粋」な関数型言語(Haskellなど)は、モナドなどのややこわい概念を使用してこれを行いますが、「純粋でない」もの(MLなど)は、これを直接許可します。

そしてもちろん、関数型言語には、ファーストクラスの関数など、プログラミングをより効率的にする多くの他の便利な機能が付属しています。


2
<< readFile( "myFile.txt")は毎回同じ文字列値を返す必要があります。あまり役に立たない。>>ファイルシステムであるグローバルを隠している限り、それは役に立つと思います。これを2番目のパラメーターと見なし、他のプロセスがfilesystem2 = write(filesystem1、fd、pos、 "string")で変更するたびにファイルシステムへの新しい参照を返し、すべてのプロセスがファイルシステムへの参照を交換できるようにする場合、オペレーティングシステムのよりきれいな画像を取得できます。
eel ghEEz 2015年

@eelghEEz、これはDatomicがデータベースに対して取るのと同じアプローチです。
Jason

1
パラダイム間の明確で簡潔な比較のための+1。一つの提案は、int double(x){ return x * (++y); }現在のものはまだ、4あろう依然として未宣伝副作用を有するが、一方以来++y6戻ります
BrainFRZ

@eelghEEz私は代替案がわからない、本当に、他の誰かですか?(純粋な)FPコンテキストに情報を導入するには、「タイムスタンプXで温度がYである」など、「測定」を行います。誰かが温度を要求する場合、それらは暗黙的にX = nowを意味するかもしれませんが、おそらく時間の普遍的な関数として温度を要求することはできませんよね?FPは不変の状態を扱い、内部および外部のソースから、可変の状態から不変の状態を作成する必要があります。インデックス、タイムスタンプなどは便利ですが、可変性とは直交しています-VCSのようにバージョン管理を行うことです。
John P

29

関数型プログラミングに「状態」がないと言うことは少し誤解を招きやすく、混乱の原因になる可能性があることに注意してください。確かに「変更可能な状態」はありませんが、操作される値を持つことはできます。それらをそのまま変更することはできません(たとえば、古い値から新しい値を作成する必要があります)。

これはかなり単純化しすぎですが、クラスのすべてのプロパティがコンストラクターで1回だけ設定され、すべてのメソッドが静的関数であるOO言語があると想像してください。メソッドで計算に必要なすべての値を含むオブジェクトを取得し、その結果で新しいオブジェクトを返す(同じオブジェクトの新しいインスタンスの場合もある)ことで、ほとんどすべての計算を実行できます。

既存のコードをこのパラダイムに変換するのは「難しい」かもしれませんが、それはコードについてまったくまったく異なる考え方が必要だからです。副作用として、ほとんどの場合、無料で並列処理の多くの機会を得ます。

補遺:( 変更が必要な値を追跡する方法の編集に関して)
それらはもちろん不変のデータ構造に保存されます...

これは推奨される「解決策」ではありませんが、これが常に機能することを確認する最も簡単な方法は、これらの不変値を「変数名」をキーとするマップ(辞書/ハッシュテーブル)のような構造に格納できることです。

明らかに実際的な解決策ではより健全なアプローチを使用しますが、これは最悪の場合、他に何も機能しなければ、呼び出しツリーを通じて持ち歩くようなマップで可変状態を「シミュレート」できることを示しています。


2
はい、タイトルを変更しました。しかし、あなたの答えはさらに悪い問題につながるようです。状態が変化するたびにすべてのオブジェクトを再作成する必要がある場合は、オブジェクトの構築のみを行うためにすべてのCPU時間を費やします。私はここでゲームプログラミングについて考えています。そこでは、画面上(および画面外)で一度にたくさんのものが動いていて、相互に対話できる必要があります。エンジン全体にはフレームレートが設定されています。これから行うことはすべて、Xミリ秒単位で行う必要があります。確かにオブジェクト全体を常にリサイクルするよりも良い方法はありますか?
メイソンウィーラー

4
それの美点は、不変性が言語ではなく実装にあるということです。いくつかのトリックを使用すると、実装で実際に状態を変更しながら、言語で状態を変更できます。たとえば、HaskellのSTモナドを参照してください。
CesarB 2009年

4
@メイソン:重要なのは、コンパイラーが状態を(スレッド)安全に変更できる場所を、ユーザーが行うよりもはるかに適切に決定できるということです。
jerryjvl 2009年

ゲームでは、速度が問題にならない部分については不変を避けるべきだと思います。不変の言語が最適化されるかもしれませんが、CPUが高速に処理するメモリを変更することほど速くはありません。ですから、命令が必要な場所が10か20あることがわかった場合、ゲームメニューなどの非常に分離された領域用にモジュール化できない限り、不変を完全に回避するべきだと思います。また、ビジネスルールのような純粋なシステムの複雑なモデリングを行うには最適だと思うので、特にゲームロジックは不変を使用するのに最適な場所になる可能性があります。
LegendLength 2017

@LegendLengthあなたは自分と矛盾しています。
Ixx 2018年

18

少し誤解があると思います。純粋な関数型プログラムには状態があります。違いは、その状態をモデル化する方法です。純粋な関数型プログラミングでは、状態は、いくつかの状態を取り、次の状態を返す関数によって操作されます。次に、状態をシーケンシングするには、状態を一連の純粋な関数に渡します。

グローバルな変更可能な状態でさえ、この方法でモデル化できます。たとえば、Haskellでは、プログラムは世界から世界への関数です。つまり、宇宙全体を渡すと、プログラムは新しい宇宙を返します。ただし、実際には、プログラムが実際に関心を持っている宇宙の部分のみを渡す必要があります。また、プログラムは実際には、プログラムが実行されるオペレーティング環境の指示となる一連のアクションを返します。

これを命令型プログラミングの観点から説明したかったのです。OK、関数型言語での非常に単純な命令型プログラミングを見てみましょう。

このコードを考えてみましょう:

int x = 1;
int y = x + 1;
x = x + y;
return x;

かなり沼地の標準命令コード。面白いことは何もしませんが、それは説明のために大丈夫です。ここに国家が関与していることに同意するでしょう。x変数の値は時間とともに変化します。ここで、新しい構文を考案して表記を少し変更しましょう。

let x = 1 in
let y = x + 1 in
let z = x + y in z 

括弧を付けて、これが何を意味するかを明確にします。

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

つまり、状態は、次の式の自由変数をバインドする一連の純粋な式によってモデル化されています。

このパターンは、IOを含め、あらゆる種類の状態をモデル化できることがわかります。


それはモナドのようなものですか?
CMCDragonkai 2014

これを検討しますか?Aはレベル1で宣言的であるBはレベル2で宣言的である、それはAが必須であると見なします。Cはレベル3で宣言型であり、Bは必須であると見なします。抽象化レイヤーを増やすと、抽象化レイヤーの下位にある言語は、それ自体よりも必須であると常に見なされます。
CMCDragonkai 14

14

ここだあなたは可変状態ずにコードを書く方法:代わりに、変更可能な変数に変化状態を置くのは、関数のパラメータにそれを置きます。そして、ループを書く代わりに、再帰的な関数を書きます。したがって、たとえば次の命令コード:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

次の関数コードになります(スキームのような構文):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

またはこのHaskellishコード

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

なぜ関数プログラミングは、(あなたが聞いてなかった)これをしたい、あなたのプログラムのより多くの部分がステートレスで、より多くの方法は何もブレークをせずに一緒に作品を置くことがあります。ステートレスパラダイムの力は、それ自体がステートレス(または純粋性)にあるのではなく、強力で再利用可能な関数を記述してそれらを組み合わせることができる能力にあります。

John Hughesのペーパー「関数型プログラミングが重要である理由」に、多くの例を含む優れたチュートリアルがあります。


13

それは同じことをするための異なる方法です。

3、5、10の数値を追加するなどの簡単な例を考えてみましょう。最初に5を追加して3の値を変更し、次に「3」に10を追加して、現在の値「 3インチ(18)。これは明らかにばかげているように見えますが、本質的には、状態ベースの命令型プログラミングがしばしば行われる方法です。実際、値3を持つさまざまな「3」を使用できますが、値は異なります。これはすべて奇妙に見えます。なぜなら、数が不変であるという非常に賢明な考えに非常に精通しているからです。

次に、値を不変にする場合に3、5、および10を追加することを検討してください。3と5を加算して別の値8を生成し、次にその値に10を加算してさらに別の値18を生成します。

これらは同じことを行う同等の方法です。必要な情報はすべて両方の方法に存在しますが、形式は異なります。1つでは、情報は状態として存在し、状態を変更するためのルールに存在します。もう1つは、情報が不変データと機能定義に存在することです。


10

私は議論に遅れましたが、関数型プログラミングで苦労している人々のためにいくつかのポイントを追加したいと思いました。

  1. 関数型言語は、命令型言語とまったく同じ状態の更新を維持しますが、更新された状態を後続の関数呼び出しに渡すことによってそれを行います。以下は、数直線を移動する非常に単純な例です。あなたの状態はあなたの現在の場所です。

最初に命令型の方法(疑似コード)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

今度は機能的な方法(疑似コード)です。命令的なバックグラウンドを持つ人々が実際にこのコードを読めるようにしたいので、私は三項演算子に大きく依存しています。したがって、三項演算子をあまり使用しない場合(私は常に私が命令型のときにそれを避けました)、ここでそれがどのように機能するかを示します。

predicate ? if-true-expression : if-false-expression

偽表現の代わりに新しい三項式を置くことにより、三項式を連鎖させることができます

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

それを念頭に置いて、これが機能バージョンです。

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

これは簡単な例です。これがゲームの世界で人々を動かしているなら、画面上にオブジェクトの現在の位置を描画し、オブジェクトの移動速度に基づいて各呼び出しに少し遅延を導入するなどの副作用を導入する必要があります。ただし、変更可能な状態は必要ありません。

  1. 教訓は、関数型言語は、異なるパラメーターで関数を呼び出すことによって状態を「変化させる」ということです。明らかにこれは実際には変数を変更しませんが、同様の効果を得る方法です。つまり、関数型プログラミングを行う場合は、再帰的に考えることに慣れる必要があります。

  2. 再帰的に考えることを学ぶことは難しいことではありませんが、練習とツールキットの両方が必要です。彼らが階乗を計算するために再帰を使用したその「Learn Java」本のその小さなセクションはそれを切り取らない。再帰から反復プロセスを作成するなどのスキルのツールキットが必要です(これが、関数型言語では末尾再帰が不可欠な理由です)、継続、不変条件など。アクセス修飾子やインターフェイスなどについて学習しないと、OOプログラミングはできません。同じこと関数型プログラミング用。

私の推奨は、Little Schemer(私が「読む」ではなく「行う」と言うことに注意してください)を実行してから、SICPのすべての演習を行うことです。完了すると、開始時とは異なる脳ができます。


8

変更可能な状態のない言語でも変更可能な状態のように見えるものを持つことは実際には非常に簡単です。

タイプの関数を考えてみましょうs -> (a, s)。Haskell構文から変換すると、これは、タイプ " s"のパラメーターを1つ取り、タイプ " a"と " s" の値のペアを返す関数を意味します。場合はs、私たちの状態の一種で、この関数は、1つの状態を取り、新しい状態を返し、おそらく値(あなたは常に別名「単位」を返すことができ()ソート「と同等のある、void」として、C / C ++で「a」タイプ)。このようなタイプの関数の複数の呼び出しをチェーンすると(1つの関数から状態が返され、次の関数に渡されます)、「可変」状態になります(実際には、各関数で新しい状態が作成され、古い状態が破棄されます) )。

変更可能な状態をプログラムが実行されている「空間」として想像し、時間の次元を考えると、理解しやすいかもしれません。瞬間t1で、「スペース」は特定の状態にあります(たとえば、一部のメモリ位置に値5がある)。後の瞬間t2で、それは別の状態にあります(たとえば、そのメモリ位置は現在値10を持っています)。これらの各「スライス」は状態であり、不変です(時間を遡って変更することはできません)。したがって、この観点からは、時間矢印の付いた全時空(変更可能な状態)から時空のスライスのセット(いくつかの不変状態)に移動し、プログラムは各スライスを値として扱い、それぞれを計算していますそれらの前のものに適用された関数として。

OK、多分それは理解するのが容易ではなかったでしょう:-)

プログラム全体の状態を値として明示的に表すことは、次の瞬間に(新しいものが作成された直後に)破棄される場合にのみ作成する必要があるとは言えないように思えるかもしれません。一部のアルゴリズムでは自然な場合もありますが、そうでない場合は別のトリックがあります。実際の状態の代わりに、マーカーにすぎない偽の状態を使用できます(この偽の状態のタイプを呼び出しますState#)。この偽の状態は言語の観点から存在し、他の値と同様に渡されますが、コンパイラーはマシンコードの生成時に完全に省略します。実行のシーケンスをマークするためだけに役立ちます。

例として、コンパイラが次の関数を提供するとします。

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

これらのHaskellのような宣言から変換readRefすると、型「a」の値へのポインターまたはハンドルに似たものと偽の状態を受け取りa、最初のパラメーターと新しい偽の状態が指す「」型の値を返します。writeRef似ていますが、代わりに指す値を変更します。

呼び出してreadRef、それから返された偽の状態を渡すとwriteRef(おそらく、途中で関連のない関数への他の呼び出しが行われます。これらの状態値は、関数呼び出しの「チェーン」を作成します)、書き込まれた値を返します。writeRef同じポインター/ハンドルを使用して再度呼び出すと、同じメモリ位置に書き込まれます。ただし、概念的には新しい(偽の)状態を返すため、(偽の)状態は依然として変更不可能です(新しいものが作成されました) ")。コンパイラーは、計算する必要のある実際の状態変数があった場合に呼び出す必要がある順序で関数を呼び出しますが、存在する唯一の状態は、実際のハードウェアの完全な(可変)状態です。

(Haskellのを知っている人は見てみましょう、したい人は、より多くの詳細を確認するために。私はいくつかの重要な詳細を、物事をたくさん簡素化し、ommited気づくでしょうControl.Monad.Stateからmtl、とのST sIO(別名ST RealWorld)モナド。)

(単に言語で変更可能な状態にするのではなく)なぜこのような遠回りの方法でそれを行うのか疑問に思うかもしれません。実際の利点は、プログラムの状態を具体化したことです。以前は暗黙的でした(プログラムの状態はグローバルであり、離れた場所でのアクションなどを可能にしました)が明示的になりました。状態を受け取って返さない関数は、状態を変更したり、影響を受けたりすることはありません。それらは「純粋」です。さらに良いことに、個別の状態スレッドを持つことができ、少しタイプマジックを使用して、命令型計算を不純にせずに純粋なものに埋め込むことができます(STHaskell のモナドは、このトリックに通常使用されるものです。上記のState#Iは実際にはGHC State# sでありSTIO モナド)。


7

関数型プログラミング状態を回避して強調します機能性。状態は実際には不変であるか、使用しているもののアーキテクチャに組み込まれている可能性がありますが、状態がないなどということは決してありません。ファイルシステムからファイルをロードするだけの静的Webサーバーと、ルービックキューブを実装するプログラムの違いを考慮してください。前者は、要求をファイルパス要求に変換し、そのファイルの内容からの応答に変換するように設計された機能の観点から実装されます。ほんの少しの構成を超えると、事実上状態は必要ありません(ファイルシステムの「状態」は、実際にはプログラムの範囲外です。プログラムは、ファイルの状態に関係なく同じように動作します)。ただし後者の場合は、キューブと、そのキューブでの操作がその状態を変更する方法のプログラム実装をモデル化する必要があります。


私がもっと反機能的だったとき、ハードドライブのようなものが変更可能であるとき、それがどのように良いかもしれないかと思いました。私のc#クラスはすべて変更可能な状態にあり、ハードドライブやその他のデバイスを非常に論理的にシミュレートできました。一方、機能に関しては、モデルとモデル化した実際のマシンの間に不一致がありました。さらに機能を掘り下げた後、私はあなたが得る利益がかなりの問題を上回ることができることに気づきました。そして、それ自体のコピーを作成するハードドライブを発明することが物理的に可能である場合、それは実際に役立ちます(ジャーナリングはすでにそうです)。
LegendLength 2017

5

他の人が与えているすばらしい答えに加えて、クラスIntegerStringJava について考えてください。これらのクラスのインスタンスは不変ですが、インスタンスを変更できないからといって、クラスが役に立たなくなるわけではありません。不変性はあなたにいくらかの安全を与えます。StringまたはIntegerインスタンスをのキーとして使用する場合、キーをMap変更することはできません。これをDateJava のクラスと比較してください。

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

マップのキーを黙って変更しました!関数型プログラミングなどの不変オブジェクトの操作は、はるかにクリーンです。どのような副作用が発生するかを推測する方が簡単です-なし!これは、プログラマにとっても、オプティマイザにとっても簡単であることを意味します。


2
私はそれを理解していますが、私の質問には答えません。コンピュータープログラムは実際のイベントまたはプロセスのモデルであることを念頭に置いて、値を変更できない場合、変化するものをどのようにモデル化するのでしょうか。
メイソンウィーラー

ええと、IntegerクラスとStringクラスを使用すると、確かに便利なことができます。それは不変という意味ではなく、変更可能な状態を持つことはできません。
エディ

@メイソンウィーラー-モノとその状態は2つの異なる「モノ」であることを理解することによって。パックマンが何であるかは、時間Aから時間Bまで変わりません。パックマンがどこであるかは変わります。時間Aから時間Bに移動すると、パックマン+状態の新しい組み合わせが得られます。これは、同じパックマン、異なる状態です。状態は変更されていません...異なる状態です。
RHSeeger 2009

4

ゲームなどの高度にインタラクティブなアプリケーションの場合、Functional Reactive Programmingはあなたの友人です。ゲームの世界のプロパティを時変値(および/またはイベントストリーム)として定式化できれば、準備は完了です!これらの式は、状態を変化させるよりも自然で意図的に明らかになる場合があります。たとえば、動くボールの場合、よく知られている法則x = v * tを直接使用できます。そして何より良いのは、このように記述されたゲームのルールは、オブジェクト指向の抽象化よりも構成が優れていることです。たとえば、この場合、ボールの速度は、ボールの衝突で構成されるイベントストリームに応じて、時間によって変化する値にすることもできます。より具体的な設計上の考慮事項については、「エルムでゲーム作る」を参照してください。


4

3

これが、FORTRANがCOMMONブロックなしで機能する方法です。渡した値とローカル変数を持つメソッドを作成します。それでおしまい。

オブジェクト指向プログラミングは私たちに状態と振る舞いを一緒にもたらしましたが、1994年にC ++から最初に出会ったとき、それは新しいアイデアでした。

ええ、私はメカニカルエンジニアだったときに関数型プログラマでしたが、知らなかったのです。


2
これがOOに固定できるものだとは思いません。OO以前の言語では、結合状態とアルゴリズムが推奨されていました。OOはそれを管理するためのより良い方法を提供しました。
Jason Baker、

「奨励」-おそらく。OOはそれを言語の明示的な部分にします。Cでカプセル化と情報の非表示を行うことができますが、OO言語を使用すると、はるかに簡単になると思います。
duffymo 2009年

2

心に留めておいてください:関数型言語はチューリング完全です。したがって、命令型言語で実行する便利なタスクはすべて関数型言語で実行できます。結局のところ、ハイブリッドアプローチについて言えることはあると思います。F#やClojureなどの言語(そして他の言語も確かにそうです)はステートレスデザインを推奨しますが、必要に応じて可変性を考慮に入れます。


2つの言語がチューリング完全であっても、同じタスクを実行できるわけではありません。つまり、同じ計算を実行できるということです。Brainfuckは完全なチューリングですが、TCPスタックを介して通信できないことはかなり確実です。
RHSeeger 2009

2
もちろんできます。たとえばCと同じハードウェアへのアクセスが与えられれば、それは可能です。それが実用的であるという意味ではありませんが、可能性はあります。
Jason Baker、

2

有用な純粋な関数型言語はありません。常に対処しなければならないレベルの可変性があります。IOはその一例です。

関数型言語は、使用するもう1つのツールと考えてください。それはあるものには良いが他のものには良いものではない。あなたが与えたゲームの例は、関数型言語を使用する最良の方法ではないかもしれません。少なくとも画面には、FPで何もできない変更可能な状態があります。問題に対する考え方や、FPで解決する問題の種類は、命令型プログラミングで慣れているものとは異なります。



-3

これはとても簡単です。関数型プログラミングでは、変数をいくつでも使用できますが、それらがローカル変数(関数内に含まれる)である場合に限ります。したがって、コードを関数でラップし、それらの関数の間で(渡されたパラメーターおよび戻り値として)値をやり取りしてください...そして、これですべてです!

次に例を示します。

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

ジョン、これは何語?
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.