web-dev-qa-db-ja.com

可変状態なしで有用なことを行うにはどうすればよいですか?

私は最近、関数型プログラミングについて多くのことを読んでおり、その大部分を理解できますが、頭をかき回すことができないのは、ステートレスコーディングです。可変状態を削除してプログラミングを単純化することは、ダッシュボードを削除して車を「単純化」することに似ているように思えます。

考えられるほぼすべてのユーザーアプリケーションには、状態がコアコンセプトとして含まれています。文書(またはSO投稿)を作成すると、状態は新しい入力ごとに変化します。ビデオゲームをプレイすると、すべての位置から始まる状態変数が大量にあります常に動き回る傾向にあるキャラクター。

この問題について議論する何かを見つけるたびに、それは本当に技術的な機能で書かれています-私は持っていない重いFP背景を想定しています。誰かがこれを誰かに説明する方法を知っていますか命令型コーディングをしっかりと理解していますが、機能面で完全なn00bは誰ですか?

編集:これまでの返信の束は、不変の値の利点を私に納得させようとしているようです。その部分を取得します。それは完全に理にかなっています。私が理解していないのは、可変変数なしで、変化しなければならない値を追跡し、絶えず変化する方法です。

242
Mason Wheeler

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

興味のある方は、 here's Erlangでのゲームプログラミングについて説明する一連の記事をご覧ください。

おそらくこの答えは気に入らないでしょうが、気に入らないでしょう get あなたがそれを使用するまで機能的なプログラム。コードサンプルを投稿して、「ここで、あなたは 見る"-しかし、構文とその基礎となる原則を理解していない場合、あなたの目はただかすみます。あなたの観点からは、私は命令型言語と同じことをしているように見えますが、意図的にプログラミングをより難しくする種類の境界。私の視点では、あなたは Blub paradox を経験しているだけです。

最初は懐疑的でしたが、数年前に関数型プログラミングトレインに飛び乗り、それに夢中になりました。関数型プログラミングの秘trickは、パターン、特定の変数の割り当てを認識し、命令型の状態をスタックに移動できることです。たとえば、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

リストの作成と操作に変更は必要ありません。ほとんどすべてのデータ構造は、同等の機能に簡単に変換できます。スタック、キュー、左翼ヒープ、赤黒木、遅延リストの不変の実装を提供するページ here を書きました。単一のコードスニペットに可変状態が含まれているわけではありません。ツリーを「変更」するには、新しいノードで新しいノードを作成します。これは、ツリー内のすべてのノードのコピーを作成する必要がないため、非常に効率的です。新しいノードで古いノードを再利用できます木。

より重要な例を使用して、私は this SQL parser も記述しました。これは完全にステートレスです(または少なくとも 僕の コードはステートレスです。基礎となるレキシングライブラリがステートレスであるかどうかはわかりません)。

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

148
Juliet

簡単な答え:できません。

それでは、不変性に関する大騒ぎは何ですか?

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

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

まず、副作用がないということは、単純なプログラムであり、推論が容易であることを意味します。プログラムの新しい部分を導入すると、既存の動作中の部分に干渉してクラッシュすることを心配する必要はありません。

第二に、これはプログラムを平凡に並列化可能にします(効率的な並列化は別の問題です)。

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

double x = 2 * x

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

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 every time。を意味します。したがって、readFile( "myFile.txt")は毎回同じ文字列値。

したがって、すべてのFPはsome状態を変化させる手段を提供する。 1つ(MLなど)はこれを直接許可します。

そしてもちろん、関数型言語には、一流の関数など、プログラミングをより効率的にする他の多くの利点があります。

73
oggy

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

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

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

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

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

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

27
jerryjvl

少し誤解があると思います。純粋な機能プログラムには状態があります。違いは、その状態がどのようにモデル化されるかです。純粋な関数型プログラミングでは、状態は何らかの状態を取り、次の状態を返す関数によって操作されます。状態を順番に並べるには、一連の純粋な関数に状態を渡すことで実現します。

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

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

次のコードを検討してください。

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を含むあらゆる種類の状態をモデル化できることがわかります。

16
Apocalisp

次に、可変状態なしでコードを記述する方法を示します:可変状態に状態を変更する代わりに、関数のパラメーターに入れます。そして、ループを書く代わりに、再帰的な関数を書きます。したがって、たとえば次の命令コード:

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

この機能コードになります(Schemeのような構文):

(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

why機能プログラマーはこれを行いたいと思っていますが(これは尋ねませんでした)、プログラムのより多くの部分がステートレスです何も壊さずに一緒に。ステートレスパラダイムの力は、ステートレス(または純粋さ)本来にあるのではなく、強力な再利用可能関数を記述し、それらを結合する能力です。

John Hughesの論文 Why Functional Programming Matters に、多くの例を含む優れたチュートリアルがあります。

11
Norman Ramsey

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

数字の3、5、10を追加するなどの簡単な例を考えてみましょう。最初に5を追加して3の値を変更し、次にその「3」に10を追加してから「 3 "(18)。これは明らかにばかげているように思えますが、本質的には、状態ベースの命令型プログラミングが頻繁に行われる方法です。実際、値3を持ちながらも異なる多くの異なる「3」を持つことができます。数字は不変であるという非常に賢明な考え方に深く染み込んでいるので、これはすべて奇妙に思えます。

ここで、値を不変にするときに3、5、および10を追加することを考えてください。 3と5を追加して別の値8を生成し、その値に10を追加してさらに別の値18を生成します。

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

10
Wedge

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

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

最初の命令的な方法(擬似コード)

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

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

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

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アクセス修飾子、インターフェースなどについて。関数型プログラミングでも同じです。

私の推奨事項は、リトルスキームを実行し(「読む」のではなく、「行う」と言うことに注意してください)、SICPのすべての演習を行うことです。完了すると、開始時とは異なる脳になります。

7

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

6
Jherico

実際、可変状態のない言語でも、可変状態のように見えるものを作成するのは非常に簡単です。

タイプがs -> (a, s)の関数を考えます。 Haskell構文から変換すると、タイプ「s」のパラメーターを1つ受け取り、タイプ「a」と「s」の値のペアを返す関数を意味します。 sが状態のタイプである場合、この関数は1つの状態を取り、新しい状態と、場合によっては値を返します(常に「unit」または()は、C/C++の「void」タイプと同様の「a」に相当します)。このようなタイプの関数呼び出しをいくつか連鎖させた場合(ある関数から返された状態を取得して次の状態に渡す)、「可変」状態になります(実際、各関数で新しい状態を作成し、古い状態を放棄します) )。

可変状態をプログラムが実行されている「スペース」として想像し、時間ディメンションを考えると理解しやすくなります。瞬間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を再度呼び出すことができ、同じメモリ位置に書き込みます—しかし、概念的には新しい(偽)状態を返すため、(偽)状態はまだimutable(新しい1つは「作成」されています)。コンパイラーは、計算する必要がある実際の状態変数があった場合に呼び出す必要がある順序で関数を呼び出しますが、存在する唯一の状態は、実際のハードウェアの完全な(可変)状態です。

(Haskellを知っている人は、物事をかなり単純化し、いくつかの重要な詳細を省略していることに気付くでしょう。詳細を確認したい人は、Control.Monad.Stateからmtl、およびST sおよびIO(別名ST RealWorld)モナド。)

(言語に単純に可変状態を持たせるのではなく)なぜこのような迂回方法でそれを行うのか疑問に思うかもしれません。本当の利点は、プログラムの状態が reified であることです。以前は暗黙的でした(プログラムの状態はグローバルで、 距離でのアクション のようなものを許可していました)が明示的になりました。状態を受け取ったり返したりしない関数は、状態を変更したり影響を受けたりすることはできません。それらは「純粋」です。さらに良いことに、別個の状態スレッドを使用することができ、ちょっとしたタイプの魔法で、純粋なものに命令型の計算を埋め込むために使用できます。それを不純にすることはありません(HaskellのSTモナドは通常このトリックに使用される; State#上記で述べたのは、実際にはGHCのState# sSTおよびIOモナドの実装で使用されます)。

6
CesarB

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

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

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

4
Eddie

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

3
thSoft
3
Paul Sweatte

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

オブジェクト指向プログラミングは状態と動作を結び付けましたが、1994年にC++から初めて遭遇したとき、それは新しいアイデアでした。

Geez、私は機械エンジニアだったときに機能的なプログラマーでしたが、それを知りませんでした!

2
duffymo

覚えておいてください:関数型言語はチューリング完全です。したがって、命令型言語で実行する有用なタスクは、関数型言語で実行できます。とはいえ、結局のところ、ハイブリッドアプローチについて何か言いたいことがあると思います。 F#やClojureなどの言語(および他の言語を使用していると確信しています)は、ステートレスデザインを促進しますが、必要に応じて可変性を考慮します。

1
Jason Baker

便利な純粋な関数型言語を持つことはできません。常に対処しなければならないレベルの可変性があります。IOは一例です。

関数型言語は、使用する別のツールと考えてください。特定の物には良いが、他のものにはない。あなたが提供したゲームの例は、関数型言語を使用する最良の方法ではないかもしれません。少なくとも画面は、FPでは何もできない可変状態になります。問題の考え方と、FPで解決する問題のタイプは、命令型プログラミングで慣れているものとは異なります。

1
Up.

多くの再帰を使用します。

F#のTic Tac Toe(関数型言語)

0
Spencer Ruport