「48時間でSchemeを書こう/変数と代入」の版間の差分

削除された内容 追加された内容
編集の要約なし
編集の要約なし
1 行
 最終的に、私たちはいいものが手に入ります。それは変数です。変数は評価の結果を保存してくれて、それをあとから参照出来るようにしてくれます。Schemeの中では、変数は新しい値でリセットすることができますし、またプログラムの実行によって、その値は変化します。これは、Haskellにとっては複雑に見えます。というのも、Haskellの実行モデルは値を返す関数の上に構築されており、それらを変えることが出来ないからです。
 
 にも関わらず、Haskellでは同じ状態をシミュレートする方法がいくつかの方法があり、全てモナドに関係します。一番シンプルなのは、おそらくステートState モナドです。このモナドは、任意の状態をモナドの中に隠しておいて、舞台裏で周囲に渡します。貴方は、パラメータとして、状態のタイプをこのモナドに記入し、普通はdoブロックの中から、get と putの機能を使って、アクセスすることができます(もし関数がIntegerを返してきて、でもStringの二組のリストに変更するなら、それは<span class="inline_code"> State [(String,String)] Integer</span>という型を持ちます)。貴方は、戻り値と最終状態を含んだペアを返す<span class="inline_code">runState myStateAction initialList</span>を通じて、初期状態を記述します。
 
 残念なことに、このStateモナドは私たちのためには上手く動いてはくれません。というのも、私たちが格納しておきたいデータの型というのは、かなり複雑だからです。もっともシンプルな環境では、変数名から値にマッピングするのを格納することによって、[(String, LispVal)]でやり通すことができますが、しかし、私たちが関数の呼び出しを扱い始めると、これらのマッピングは、任意の深さの、入れ子になった環境のスタックになるでしょう。そして、私たちがクロージャーを追加したとき、この環境は環境はきっと任意の関数の値に保存され、そしてプログラム全体から返されるかもしれません。事実、私たちが許せ容認できなくなるまで、値を保存し、そしてrunStateモナド完全に潰されるでしょう。
 
 代わりに、私たちは''State スレッド''と呼ばれるものを使って、私たちの為に、この集合状態をHaskellに管理させましょう。これは変数を得たり、設定する為の機能を使って、他のプログラム言語と同じように、可変変数を扱うことができるようになります。Stateスレッドには二つの種類があります。the [http://www.haskell.org/ghc/docs/latest/html/libraries/base/Control-Monad-ST.html ST monad] は残りのプログラムへ状態を回避させることなしに、単体で実行されるような、ステートフルな経産変数を作り出すSTモナドです。また、[http://www.haskell.org/ghc/docs/latest/html/libraries/base/Data-IORef.html IORef モジュール]は、IO monadの中で、私たちにステートフルな変数を使わせてくれます。私たちの状態を、どんな方法でもいいので、仲介する必要がありますので(REPLの中で、行間を持続させ、最終的に、言語自身にIO functionを持つとしても)、私たちはIO Refsを扱います。
 
 私たちは[http://www.haskell.org/ghc/docs/6.4/html/libraries/base/Data.IORef.html Data.IORef]をimportすることから始めましょう。そして、私たちの環境状態のため、型を決定します。
 
import Data.IORef
13 行
type Env = IORef [(String, IORef LispVal)]
 
 これで、変化が多いLispValを文字列として対応した、IORefが持つリストとして、Envを定義できます。リストかそれ自身両方のための、そして個々の値のために、IO Refsが必要です。というのも、プログラムSchemeが環境を変化させるためには、二つの方法があるからです。まず一つに、個々の変数の値を変化させ、明らかな変化を、この環境で共有するためのいくつかの機能を、<span class="inline_lisp">set!</span>で使えます(Schemeはスコープを入れ子にすることを許可しています。なので、外のスコープにある変数も、全ての内部スコープから見えています)。また、全てのサブシークエントの式から見えるべきである、新しい変数を追加するための<span class="inline_lisp">define</span>も使うことができます。
 IORefs は ただ、IOモナドの内部の中でのみ使うことができます。私たちは、空の環境を作るアクションのための手伝いを欲しがるでしょう。ただ、空のリストである [] は使うことができません。というのは、IORefsにアクセスするときは、必ず順番が決まっていなければならないからで、だから私たちの空っぽな環境の型は、ただの空のEnvの代わりに、IO Envになります。
 
nullEnv :: IO Env
nullEnv = newIORef []
 
 ここから、ことはちょっとだけ複雑になります。というのも同時に''二つ''のモナドをあつかないといけないからです。覚えておいてください。私たちはまた束縛されていない変数のようなものを扱うためのErrorモナドが同様に必要になります。例外を投げることができる、IOの機能性が必要になるパーツは仲介され、そして私たちはただ全ての例外をキャッチしたり、またはIOモナドからただ値だけを返してもらうということができなくなります。
 
 多様なモナドが持つ機能を結合させるような、モナド変化として知られるようなメカニズムを、Haskellは供給しています。私たちは、きっとその一つを使うことでしょう。 - [http://www.haskell.org/ghc/docs/6.4/html/libraries/mtl/Control.Monad.Error.html#t%3aErrorT ErrorT] - これは、IOモナドのトップで、エラーハンドリングの機能性があるレイヤーを与えます。私たちの最初のステップは、私たちの結合したモナドのために、別の名前の型を作ってあげます。
28 行
 ThrowsErrorやIOThrowsErrorのようなものは、実際にタイプのコンストラクタなのです。というのも、私たちは最後の引数や関数の返り値のタイプへと入れるからです。しかし、ErrorTは、まっさらな、古い、おのおのの引数から、一つ以上の引数を取ります。私たちは、自分たちのエラーを扱う機能のレイヤーにある、モナドの型を明示しなければならないのです。なので、LispErrorを投げるIOの行動を含めることができるモナドを作ります。
 
 私たちは、IOThrowsErrorとThrowsErrorのミックスがあります。しかし違う型によるアクションは同じdoブロックの中に含めることが出来ないのです。それが、たとえ本質的には同じ機能を提供していた、としても。Haskellは、低い型(IO)の値を、結合したモナドへ送るためのメカニズムが、既に提供されています。 [http://www.haskell.org/all_about_monads/html/transformers.html#lifting lifting]です。残念なことに、まだ変形していない、上位型の値を豪勢合成モナドの中へと運ぶ似たようなサポートはないので、私たちは自分たちでそういうものを書く必要があります。
 
liftThrows :: ThrowsError a -&gt; IOThrowsError a
58 行
(lookup var env)
 
 これは、前出した関数のように、IORefから実際の環境を取り出し始めます。しかし、getVarはIOthrowsErrorを使います。というのも、これも同様にいくつかのエラーを扱う必要があるからです。結局、私たちは、readIORefを取り出すための関数である、[http://www.haskell.org/ghc/docs/6.4/html/libraries/mtl/Control.Monad.Trans.html#v%3aliftIO liftIO]関数を使う必要が出てきました。同じように、私たちが値を返すときも、liftIOを使います。IOThrowsErrorアクションを作り出すためのreadIORefは、IORefの返り値を返します。しかし、私たちはエラーを投げてもらうために、liftIOを使う必要はなくなりました。というのも、throwErrorは[http://www.haskell.org/ghc/docs/6.4/html/libraries/mtl/Control.Monad.Error.html#t%3aMonadError MonadError typeclass]に定義されているからで、ErrorTはこのインスタンスです。
 
 関数に、値をセットする関数を作りましょう。