「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の機能があればとてもいいと思います。同様に、私たちがインタプリターを起動する度に、本当に関数の中で長ったらしい記述をするより、コードが書いてあるファイルを読み込めて、それを評価できたらいいなと思います。
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 functions every time we start the interpreter, so it would be nice to load files of code and execute them.
 
 必要になる、まず最初のことは、LispValための新しいコンストラクタです。PrimitiveFuncsはIOモナドが含まない特別な型を持っています。ですので、他のIOと振る舞うことができません。私たちは、IOのように振る舞える、献身的なコンストラクタを求めることになります。
The first thing we'll need is a new constructor for LispVals. PrimitiveFuncs have a specific type signature that doesn't include the IO monad, so they can't perform any IO. We want a dedicated constructor for primitive functions that perform IO:
 
| IOFunc ([LispVal] -> IOThrowsError LispVal)
 
While we're at it, let's also define a constructor for the Scheme data type of a [http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.6.1 port]. Most of our IO functions will take one of these to read from or write to:
 
 この中で、私たちは、同様に [http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.6.1 port] という、Schemeのデータタイプの為のコンストラクタを定義します。私たちのIO関数の多くは、これらの一つを取って、読み書きされます。
| Port Handle
 
A [http://www.haskell.org/onlinereport/io.html#sect21 Handle] is basically the Haskell notion of a port: it's an opaque data type, returned from openFile and similar IO actions, that you can read and write to.
 
For completeness, we ought to provide showVal methods for the new data types:
 
 [http://www.haskell.org/onlinereport/io.html#sect21 Handle] は、基本的な portの Haskell notionで、openFIleから返し、またIOアクションと似ていて、貴方は読み書きが出きるようになります。
 
 完全を期すために、私たちは新しいデータ型のために、showValメソッドを供給するべきでしょう。
showVal (Port _) = "<IO port>"
showVal (IOFunc _) = "<IO primitive>"
 
This'll let the REPL function properly and not crash when you use a function that returns a port.
 
 これは、きっと、REPL機能を適切にし、portを返す関数を使うときに、クラッシュしないようにしてくれるでしょう。
We also need to update apply, so that it can handle IOFuncs:
 
 私たちは同様に、applyをアップデートする必要があります。なので、IOFuncsを扱います。
apply (IOFunc func) args = func args
 
apply (IOFunc func) args = func args
We'll need to make some minor changes to our parser to support [http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9.html#%_sec_6.6.4 load]. Since Scheme files usually contain several definitions, we need to add a parser that will support several expressions, separated by whitespace. And it also needs to handle errors. We can re-use much of the existing infrastructure by factoring our basic readExpr so that it takes the actual parser as a parameter:
 
 [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 -&gt;</span> String -&gt; ThrowsError a
38 ⟶ 39行目:
</span>
 
 再び、readExpr及びreadExprListの両方を、新しく命名されたreadOrThrowが特別化されたものとして考えます。私たちは、自分たちのREPLを単純な評価として読み込みます。私たちは、readExprListを、loadの中から、プログラムの中から読み込むために使うでしょう。
Again, think of both readExpr and readExprList as specializations of the newly-renamed readOrThrow. We'll be using readExpr in our REPL to read single expressions; we'll be using readExprList from within load to read programs.
 次に、ただ存在するprimitiveリストみたいに構成された、IO primitvesの新しいリストが必要になるでしょう。
 
Next, we'll want a new list of IO primitives, structured just like the existing primitive list:
 
ioPrimitives :: [(String, [LispVal] -&gt; IOThrowsError LispVal)]
54 ⟶ 53行目:
("read-all", readAll)]
 
 ここでは、違いは型のシグネチャの違いです。残念なことに、私たちは、存在するprimitiveのリストを使うことが出来ません。というのも、リストは型の違いによる要素に含めることができないからです。私たちは同様に、primitiveBindingsの定義を新しいprimitivesに新しく追加するよう編集する必要があります。
The only difference here is in the type signature. Unfortunately, we can't use the existing primitive list because lists cannot contain elements of different types. We also need to change the definition of primitiveBindings to add our new 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を呼び出します。
We've generalized makeFunc to take a constructor argument, and now call it on the list of ioPrimitives in addition to the plain old primitives.
 
Now we start defining the actual functions. applyProc is a very thin wrapper around apply, responsible for destructuring the argument list into the form apply expects:
 
 今、私たちは実際の関数を定義し始めています。applyProcはapplyのまわりを薄く包み込み、解きほぐされた引数のリストから、applyが期待しているものへと反応します。
applyProc :: [LispVal] -&gt; IOThrowsError LispVal
71 ⟶ 68行目:
applyProc (func : args) = apply func args
 
 makeProtはHaskellの関数であるopenFileを包み込み、右の型にコンバートし、そしてPortコンストラクタの中で返り値をラップします。これはIOMode、open-input-fileの為の、ReadMode及びWriteModeに、部分的に適応します。
makePort wraps the Haskell function openFile, converting it to the right type and wrapping its return value in the Port constructor. It's intended to be partially-applied to the IOMode, ReadMode for open-input-file and WriteMode for open-output-file:
 
makePort :: IOMode -&gt; [LispVal] -&gt; 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の結果に送ります。
readProc (named to avoid a name conflict with the built-in read) wraps the Haskell hGetLine and then sends the result to parseExpr, to be turned into a LispVal suitable for Scheme:
 
91 ⟶ 88行目:
readProc [Port port] = (liftIO $ hGetLine port) &gt;&gt;= liftThrows . readExpr
 
Notice how  "hGetLine port" is of typeが如何に <span class="inline_code">IO String</span> yet の型となるのか、また如何にreadExpr is of type <span class="inline_code">String -&gt; ThrowsError LispVal</span>, so they both need to be converted の型になるのかに気をつけてください。そして、両方とも、(with liftIO and liftThrows, respectivelyや、おのおのと共に) to the IOThrowsError monad. Only then can they be piped together with the monadic bind operator.モナドに変換する必要があります。彼らができることは、モナディックバインドオペレーターを使って彼らを橋渡しすることです。
 
writeProc converts a LispVal to a string and then writes it out on the specified port:
 
 writeProcはLispValをストリングにして、特別なポートにコンバートして書き直すということです。
writeProc :: [LispVal] -&gt; IOThrowsError LispVal
100 ⟶ 96行目:
writeProc [obj, Port port] = liftIO $ hPrint port obj &gt;&gt; (return $ Bool True)
 
 私たちは、プリントアウトするためのオブジェクトの上に、明確にshowを呼び出す必要性がなくなりました。というのも、hPrintはShow型の値を取るからです。これは、私たちのために、自動的にshowを呼び出します。これは、ShowインスタンスをLispValに作るのを悩ませる原因にもなります。同様に、私たちは、自動的な変換を使うことができませんし、またshowValそれ自身を呼ばなければなりません。多くの他のHaskell関数は、同様にShowインスタンスを取り、もし他のIO primitivesをこれに拡張するなら、重要な仕事として保管できるでしょう。
We don't have to explicitly call show on the object we're printing, because hPrint takes a value of type Show a. It's calling show for us automatically. This is why we bothered making LispVal an instance of Show; otherwise, we wouldn't be able to use this automatic conversion and would have to call showVal ourselves. Many other Haskell functions also take instances of Show, so if we'd extended this with other IO primitives, it could save us significant labor.
 readContentsは、全体のファイルを、メモリの中で文字列に変換します。これはHaskellのもつreadFileの簡単なラッパーで、ただIOアクションをIOThowsErrorに渡し、Stringコンストラクタの中でそれをラップします。
 
readContents reads the whole file into a string in memory. It's a thin wrapper around Haskell's readFile, again just lifting the IO action into an IOThrowsError action and wrapping it in a String constructor:
 
readContents :: [LispVal] -&gt; IOThrowsError LispVal
readContents [String filename] = liftM String $ liftIO $ readFile filename
 
 助けになる関数である"load"は、Schemeの持つロードが何を読み込んでいるものを、読み込むことが出来ません(私たちはあとでこれを取り扱うでしょう)。むしろ、これはファイルの全ての節を分割して、読み込んでいるのが原因だからです。二つの部分を使いましょう。(値のリストを返す)readAllと、(Schemeの表現として、値を評価する)loadとです。
The helper function "load" doesn't do what Scheme's load does (we handle that later). Rather, it's responsible only for reading and parsing a file full of statements. It's used in two places: readAll (which returns a list of values) and load (which evaluates those values as Scheme expressions).
 
load :: String -&gt; IOThrowsError [LispVal]
load filename = (liftIO $ readFile filename) &gt;&gt;= liftThrows . readExprList
 
 それゆえ、readAllはListコンストラクタと共に値を返すラップをします。
readAll then just wraps that return value with the List constructor:
 
readAll :: [LispVal] -&gt; IOThrowsError LispVal
readAll [String filename] = liftM List $ load filename
 
 実際の、Schemeのload関数を取り扱う手段は、少々トリッキーになります。というのは、loadは束縛をローカル環境へと取り入れるからです。しかし、Applyは環境を引数として取ることがでいないし、primitve関数(や他の関数)の為に、これを行う方法もありません。私たちは、特別なかたちとして、この周辺にloadの手段を作りましょう。
Implementing the actual Scheme load function is a little tricky, because load can introduce bindings into the local environment. Apply, however, doesn't take an environment argument, and so there's no way for a primitive function (or any function) to do this. We get around this by implementing load as a special form:
 
eval env (List [Atom "load", String filename]) =
load filename &gt;&gt;= liftM last . mapM (eval env)
 
 最終的に、私たちは自分たちのrunOne関数を、コマンドラインからの単独な表現として評価する代わりとして、変化させることができました。これは、プログラムとして評価し、実行するためのファイル名を取ります。追加されたコマンドラインの引数は、Schemeプログラムの中で、引数リストに束縛されます。
Finally, we might as well change our runOne function so that instead of evaluating a single expression from the command line, it takes the name of a file to execute and runs that as a program. Additional command-line arguments will get bound into a list "args" within the Scheme program:
 
runOne :: [String] -&gt; IO ()
135 ⟶ 125行目:
&gt;&gt;= hPutStrLn stderr
 
 ここはちょっと難解なので、徐々にまいりましょう。最初の分は、元のprimitve束縛を取っていて、bindVarsに投げて、そして最初の引数以外の全てのStringの解釈を含んだリストを束縛した、変数名"args"を追加します(最初の引数は、評価されたファイル名です)。そして、ユーザーがタイプしたかどうか、またはそれを評価したかどうかで、Schemeのかたちを作り("args1"を読み込み)ます。その結果、文字列に変化します。(覚えておいてください。これはエラーを受け取る前に、このようにしてやる必要があります。というのも、エラーはそれらを文字列にコンバートするために扱うもので、まずは型をあわせないといけません)、そして、全体のIOThrowsErrorアクションを走らせます。そして、私たちは STDERRを結果としてプリントします。(伝統的なUNIXの大会では、STDOUTはプログラムのアウトプットの為だけに使われるべきで、いかなるエラーメッセージもSTDERRで扱われるべきです。この場合だと、私たちは同様に、このプログラムの最終的な説の値を返すときにプリントされますが、一般的にはどんなものでも意味がないものです)
That's a little involved, so let's go through it step-by-step. The first line takes the original primitive bindings, passes that into bindVars, and then adds a variable named "args" that's bound to a List containing String versions of all but the first argument. (The first argument is the filename to execute.) Then, it creates a Scheme form (load "arg1"), just as if the user had typed it in, and evaluates it. The result is transformed to a string (remember, we have to do this before catching errors, because the error handler converts them to strings and the types must match) and then we run the whole IOThrowsError action. Then we print the result on STDERR. (Traditional UNIX conventions hold that STDOUT should be used only for program output, with any error messages going to stderr. In this case, we'll also be printing the return value of the last statement in the program, which generally has no meaning to anything.)
 
Then we change main so it uses our new runOne function. Since we no longer need a third clause to handle the wrong number of command-line arguments, we can simplify it to an if statement:
 
 そして、新しいrunOne関数によって、mainを変化させましょう。私たちはもはやコマンドライン引数の間違った値を扱うために、三つ目の節が必要となりましたので、私たちはif文を簡単にこここに追加しましょう。
main :: IO ()
main = do args &lt;- getArgs
if null args then runRepl else runOne $ args
 
{{auto navigation|Towards a Standard Library|Defining Scheme Functions}}
{{auto category}}