「48時間でSchemeを書こう/IOプリミティブの作成」の版間の差分
削除された内容 追加された内容
ページの作成:「Our Scheme can't really communicate with the outside world yet, so it would be nice if we could give it some I/O functions. Also, it gets really tedious typing in funct...」 |
編集の要約なし |
||
1 行
私たちのSchemeは外部の世界と未だに対話することが出来ません。もし何かしらのI/Oの機能があればとてもいいと思います。同様に、私たちがインタプリターを起動する度に、本当に関数の中で長ったらしい記述をするより、コードが書いてあるファイルを読み込めて、それを評価できたらいいなと思います。
必要になる、まず最初のことは、LispValための新しいコンストラクタです。PrimitiveFuncsはIOモナドが含まない特別な型を持っています。ですので、他のIOと振る舞うことができません。私たちは、IOのように振る舞える、献身的なコンストラクタを求めることになります。
| IOFunc ([LispVal] -> IOThrowsError LispVal)
この中で、私たちは、同様に [http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.6.1 port] という、Schemeのデータタイプの為のコンストラクタを定義します。私たちのIO関数の多くは、これらの一つを取って、読み書きされます。
| Port Handle
[http://www.haskell.org/onlinereport/io.html#sect21 Handle] は、基本的な portの Haskell notionで、openFIleから返し、またIOアクションと似ていて、貴方は読み書きが出きるようになります。
完全を期すために、私たちは新しいデータ型のために、showValメソッドを供給するべきでしょう。
showVal (Port _) = "<IO port>"
showVal (IOFunc _) = "<IO primitive>"
これは、きっと、REPL機能を適切にし、portを返す関数を使うときに、クラッシュしないようにしてくれるでしょう。
私たちは同様に、applyをアップデートする必要があります。なので、IOFuncsを扱います。
apply (IOFunc func) args = func args▼
▲ apply (IOFunc func) args = func args
[http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.6.4 load]をサポートするため、私たちのパーサーをマイナーチェンジする必要があります。普通、Schemeファイルは複数の定義を含んでおり、パーサーにも、幾つかの評価をサポートしたり、あるいは空白によって分割することを追加しないといけません。そして、同様に扱い時のエラーも必要になります。私たちは、実際のパーサーがパラメーターを取れるような基礎的なreadExprを作ることによって、殆どの存在する基盤を再利用することが出来ます。
<span class="changed_code">readOrThrow :: Parser a -></span> String -> ThrowsError a
38 ⟶ 39行目:
</span>
再び、readExpr及びreadExprListの両方を、新しく命名されたreadOrThrowが特別化されたものとして考えます。私たちは、自分たちのREPLを単純な評価として読み込みます。私たちは、readExprListを、loadの中から、プログラムの中から読み込むために使うでしょう。
次に、ただ存在するprimitiveリストみたいに構成された、IO primitvesの新しいリストが必要になるでしょう。
ioPrimitives :: [(String, [LispVal] -> IOThrowsError LispVal)]
54 ⟶ 53行目:
("read-all", readAll)]
ここでは、違いは型のシグネチャの違いです。残念なことに、私たちは、存在するprimitiveのリストを使うことが出来ません。というのも、リストは型の違いによる要素に含めることができないからです。私たちは同様に、primitiveBindingsの定義を新しいprimitivesに新しく追加するよう編集する必要があります。
primitiveBindings :: IO Env
62 ⟶ 60行目:
where makeFunc <span class="changed_code">constructor</span> (var, func) = (var, <span class="changed_code">constructor</span> func)
私たちは、コンストラクターの引数を取るためにmakeFuncを作り、そして今や過去のまっさらなprimitivesにioPrimitiveのリストを追加してmakeFunkを呼び出します。
今、私たちは実際の関数を定義し始めています。applyProcはapplyのまわりを薄く包み込み、解きほぐされた引数のリストから、applyが期待しているものへと反応します。
applyProc :: [LispVal] -> IOThrowsError LispVal
71 ⟶ 68行目:
applyProc (func : args) = apply func args
makeProtはHaskellの関数であるopenFileを包み込み、右の型にコンバートし、そしてPortコンストラクタの中で返り値をラップします。これはIOMode、open-input-fileの為の、ReadMode及びWriteModeに、部分的に適応します。
makePort :: IOMode -> [LispVal] -> IOThrowsError LispVal
makePort mode [String filename] = liftM Port $ liftIO $ openFile filename mode
ClosePortは同様に、Haskellの同等の手続きと、同じhCloseをラップします。
closePort also wraps the equivalent Haskell procedure, this time hClose:
84 ⟶ 81行目:
closePort _ = return $ Bool False
Schemeのために、LispValに適切に切り替えるために、(ビルドインされたreadと、名前がコンフリクトしないように避けられた)readProcは、hGetLineをラップし、そしてparseExprの結果に送ります。
91 ⟶ 88行目:
readProc [Port port] = (liftIO $ hGetLine port) >>= liftThrows . readExpr
writeProcはLispValをストリングにして、特別なポートにコンバートして書き直すということです。
writeProc :: [LispVal] -> IOThrowsError LispVal
100 ⟶ 96行目:
writeProc [obj, Port port] = liftIO $ hPrint port obj >> (return $ Bool True)
私たちは、プリントアウトするためのオブジェクトの上に、明確にshowを呼び出す必要性がなくなりました。というのも、hPrintはShow型の値を取るからです。これは、私たちのために、自動的にshowを呼び出します。これは、ShowインスタンスをLispValに作るのを悩ませる原因にもなります。同様に、私たちは、自動的な変換を使うことができませんし、またshowValそれ自身を呼ばなければなりません。多くの他のHaskell関数は、同様にShowインスタンスを取り、もし他のIO primitivesをこれに拡張するなら、重要な仕事として保管できるでしょう。
readContentsは、全体のファイルを、メモリの中で文字列に変換します。これはHaskellのもつreadFileの簡単なラッパーで、ただIOアクションをIOThowsErrorに渡し、Stringコンストラクタの中でそれをラップします。
readContents :: [LispVal] -> IOThrowsError LispVal
readContents [String filename] = liftM String $ liftIO $ readFile filename
助けになる関数である"load"は、Schemeの持つロードが何を読み込んでいるものを、読み込むことが出来ません(私たちはあとでこれを取り扱うでしょう)。むしろ、これはファイルの全ての節を分割して、読み込んでいるのが原因だからです。二つの部分を使いましょう。(値のリストを返す)readAllと、(Schemeの表現として、値を評価する)loadとです。
load :: String -> IOThrowsError [LispVal]
load filename = (liftIO $ readFile filename) >>= liftThrows . readExprList
それゆえ、readAllはListコンストラクタと共に値を返すラップをします。
readAll :: [LispVal] -> IOThrowsError LispVal
readAll [String filename] = liftM List $ load filename
実際の、Schemeのload関数を取り扱う手段は、少々トリッキーになります。というのは、loadは束縛をローカル環境へと取り入れるからです。しかし、Applyは環境を引数として取ることがでいないし、primitve関数(や他の関数)の為に、これを行う方法もありません。私たちは、特別なかたちとして、この周辺にloadの手段を作りましょう。
eval env (List [Atom "load", String filename]) =
load filename >>= liftM last . mapM (eval env)
最終的に、私たちは自分たちのrunOne関数を、コマンドラインからの単独な表現として評価する代わりとして、変化させることができました。これは、プログラムとして評価し、実行するためのファイル名を取ります。追加されたコマンドラインの引数は、Schemeプログラムの中で、引数リストに束縛されます。
runOne :: [String] -> IO ()
135 ⟶ 125行目:
>>= hPutStrLn stderr
ここはちょっと難解なので、徐々にまいりましょう。最初の分は、元のprimitve束縛を取っていて、bindVarsに投げて、そして最初の引数以外の全てのStringの解釈を含んだリストを束縛した、変数名"args"を追加します(最初の引数は、評価されたファイル名です)。そして、ユーザーがタイプしたかどうか、またはそれを評価したかどうかで、Schemeのかたちを作り("args1"を読み込み)ます。その結果、文字列に変化します。(覚えておいてください。これはエラーを受け取る前に、このようにしてやる必要があります。というのも、エラーはそれらを文字列にコンバートするために扱うもので、まずは型をあわせないといけません)、そして、全体のIOThrowsErrorアクションを走らせます。そして、私たちは STDERRを結果としてプリントします。(伝統的なUNIXの大会では、STDOUTはプログラムのアウトプットの為だけに使われるべきで、いかなるエラーメッセージもSTDERRで扱われるべきです。この場合だと、私たちは同様に、このプログラムの最終的な説の値を返すときにプリントされますが、一般的にはどんなものでも意味がないものです)
そして、新しいrunOne関数によって、mainを変化させましょう。私たちはもはやコマンドライン引数の間違った値を扱うために、三つ目の節が必要となりましたので、私たちはif文を簡単にこここに追加しましょう。
main :: IO ()
main = do args <- getArgs
if null args then runRepl else runOne $ args
|