関数型プログラミング言語が状態を保存できない場合、ユーザーからの入力の読み取り (つまり、それを「保存」する方法) や、そのためのデータの保存などの簡単なことをどのように行うのでしょうか?
お察しのとおり、関数型プログラミングには状態がありませんが、データを格納できないわけではありません。違いは、(Haskell)ステートメントを次の行に沿って書くと
let x = func value 3.14 20 "random"
in ...
x
の値は常に同じであることが保証されています...
。何も変更することはできません。同様に、関数f :: String -> Integer
(文字列を取得して整数を返す関数) があるf
場合、引数を変更したり、グローバル変数を変更したり、データをファイルに書き込んだりしないことを確認できます。上記のコメントで sepp2k が述べたように、この非可変性はプログラムについての推論に非常に役立ちます: データを折りたたみ、スピンドル、および切断する関数を記述し、新しいコピーを返すことでそれらを連鎖させることができます。それらの関数呼び出しのうち、「有害」なことは何でもできます。それx
は常にであり、誰かがの宣言の間のどこかにx
書いたことを心配する必要はありません。x := foo bar
x
それは不可能だからです。
では、ユーザーからの入力を読み取りたい場合はどうすればよいでしょうか。KennyTM が言ったように、不純な関数とは、全世界を引数として渡された純粋な関数であり、その結果と世界の両方を返すという考え方です。もちろん、実際にこれを行う必要はありません。1 つは、これは恐ろしく扱いにくいことです。もう 1 つは、同じワールド オブジェクトを再利用するとどうなるかということです。したがって、これはどういうわけか抽象化されます。Haskell は IO タイプで処理します。
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
これmain
は、何も返さない IO アクションであることを示しています。このアクションを実行することは、Haskell プログラムを実行することを意味します。ルールは、IO タイプが IO アクションを決してエスケープできないということです。このコンテキストでは、 を使用してそのアクションを紹介しdo
ます。したがって、 をgetLine
返しますIO String
。これは、2 つの方法で考えることができます。1 つ目は、実行時に文字列を生成するアクションとして。2 つ目は、不純に取得されたために IO によって「汚染された」文字列として。前者の方が正確ですが、後者の方がより役に立ちます。は<-
からString
を取り出してIO String
格納しstr
ますが、IO アクションを実行しているため、元に戻す必要があるため、「エスケープ」することはできません。次の行は整数の読み取りを試み ( reads
)、最初に成功した一致を取得します (fst . head
); これはすべて純粋 (IO なし) であるため、let no = ...
. no
その後、 でとstr
の両方を使用できます...
。getLine
このように、純粋でないデータ (からstr
) と純粋なデータ ( )を格納しましlet no = ...
た。
IO を操作するためのこのメカニズムは非常に強力です。これにより、プログラムの純粋なアルゴリズム部分を純粋でないユーザー操作側から分離し、これを型レベルで強制することができます。minimumSpanningTree
関数は、コード内の他の場所を変更したり、ユーザーにメッセージを書き込んだりすることはできません。安全です。
Haskell で IO を使用するために知っておく必要があるのはこれだけです。それだけでよければ、ここでやめてください。しかし、それが機能する理由を理解したい場合は、読み続けてください。(そして、これは Haskell に固有のものになることに注意してください。他の言語では別の実装が選択される可能性があります。)
したがって、これはおそらくちょっとしたごまかしのように見え、どういうわけか純粋な Haskell に不純物を追加しました。しかし、そうではありません — 純粋な Haskell 内で IO 型を完全に実装できることがわかりました ( が与えられている限りRealWorld
)。アイデアは次のとおりです: IO アクションIO type
は function と同じでRealWorld -> (type, RealWorld)
、現実世界を取り、型のオブジェクトとtype
変更された の両方を返しますRealWorld
。次に、狂気に陥ることなくこの型を使用できるように、いくつかの関数を定義します。
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
最初のものは、何もしない IO アクションについて話すことを可能にします。return 3
これは、現実世界を照会せずに を返すだけの IO アクション3
です。「>>=
バインド」と発音されるオペレーターは、IO アクションを実行できるようにします。IO アクションから値を抽出し、関数を介してその値と実世界を渡し、結果の IO アクションを返します。>>=
は、IO アクションの結果が決してエスケープできないという規則を強制することに注意してください。
次に、上記main
を次の通常の関数アプリケーションのセットに変換できます。
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Haskell ランタイムmain
は最初の でジャンプスタートし、RealWorld
設定は完了です! すべてが純粋で、派手な構文を持っているだけです。
[編集: @Conal が指摘しているように、これは実際には Haskell が IO を行うために使用するものではありません。このモデルは、並行性を追加したり、実際に IO アクションの途中で世界を変更したりすると破綻するため、Haskell がこのモデルを使用することは不可能です。逐次計算でのみ正確です。したがって、Haskell の IO はちょっとした回避策である可能性があります。そうでなくても、これほどエレガントではないことは確かです。@Conal の観察によると、Simon Peyton-Jones がTackling the Awkward Squad [pdf]のセクション 3.1 で述べていることを参照してください。彼は、これらの線に沿った代替モデルに相当するものを提示しますが、その複雑さのためにそれを削除し、別の方法を取ります.]
繰り返しますが、これは、Haskell で IO と一般的な可変性がどのように機能するかを (かなり) 説明しています。これだけ知りたい場合は、ここで読むのをやめてください。最後にもう 1 度理論を説明したい場合は、読み続けてください。しかし、この時点で、私たちはあなたの質問とはかけ離れていることを忘れないでください。
最後に、この構造 ( return
andを使用したパラメトリック型>>=
) は非常に一般的であることがわかります。それはモナドと呼ばれ、do
記法 ,return
は>>=
それらのどれでも動作します。ここで見たように、モナドは魔法ではありません。魔法のすべては、do
ブロックが関数呼び出しに変わることです。RealWorld
タイプは、魔法を見る唯一の場所です。リストコンストラクターのような型[]
もモナドであり、純粋でないコードとは何の関係もありません。
これで、モナドの概念について (ほぼ) すべてを理解できましたが (満たさなければならないいくつかの法則と正式な数学的定義を除く)、直感が欠けています。オンラインには途方もない数のモナドのチュートリアルがあります。私はこれが好きですが、オプションがあります。ただし、これはおそらく役に立ちません。直観を得る唯一の本当の方法は、それらを使用し、適切なタイミングでいくつかのチュートリアルを読むことです.
ただし、IO を理解するためにその直感は必要ありません。モナドを完全に一般的に理解することはケーキの飾りですが、今すぐ IO を使用できます。最初の機能を示した後、それを使用できますmain
。IO コードを純粋でない言語であるかのように扱うことさえできます! しかし、基礎となる関数表現があることを覚えておいてください: 不正行為はありません。
(PS: 長くなってすみません。少し遠くまで行きました。)