他の (命令的) 言語に関する本をよく読む人なら、他の言語のチュートリアルでよく書かれているプログラム (ユーザーに名前を尋ね、その名前を使って挨拶を返すようなもの) がなぜ出てこないのか、不思議に思うかもしれない。その理由は単純だ。純粋な関数型言語であるため、ユーザー入力などの処理をどのように扱えばよいかがあまりはっきりしていないからだ。
何はともあれ、キーボードからの入力文字列を読み込む関数があったとしよう。この関数を2度呼び出し、1度目に何かを入力し、2度目にまた別の何かを入力したとき、返される2つの値が異なるなら、それはもはや関数ではない。この問題に対する解決策は、構成的数学の1分野である圏論の底のほうから見いだすことができる。それがモナド (monad) である。まだ正式にモナドについて論じる準備はできていないが、ここでは単純に、モナドは入出力のような操作を表現する便利な手段であると考えることにしよう。ここで触れるモナドの詳細は第5章で論じている。また、第9章をモナドについての章に充てている。
さて、対話的な関数を書こうとしているとしよう。そのためには、キーワード do を使用する。doを使えば処理の順序を指定できる (Haskellはlazyな言語なので、通常は処理が評価される順序を指定できないことを思い出してほしい)。つまり、ユーザーに名前と住所を直接尋ねる単純なプログラムを書くには、以下のようなコードを "Name.hs" に入力する。
module Main
where
import IO
main = do
hSetBuffering stdin LineBuffering
putStrLn "Please enter your name: "
name <- getLine
putStrLn ("Hello, " ++ name ++ ", how are you?")■注■ 2つめの
putStrLnは括弧が必要だが、1つめでは不要である。それは、++より関数のほうが結びつきが強いからだ。つまり、2つめは括弧を付けないと(putStrLn "Hello, ") ++ name ++ ...と解釈されてしまう。このコードをインタープリタにロードし、"main" と入力すれば実行できる。コンパイルしてコマンドラインから実行することもできる。以下に示したのは、対話モードでの実行結果だ。
Main> main
Please enter your name:
Hal
Hello, Hal, how are you?
Main>ここでは対話処理を行っている。さて、コードに戻って少し見てみよう。コンパイルできるように、モジュール名を "Main" としている。プログラムの起動時に実行する関数がどれかコンパイラがわかるように、中心となる関数は "
main" という名前にする。4行目でIOライブラリをインポートし、IO関連の関数を利用できるようにしている。7行目はdoから始まっている。このようにすると、Haskellはコマンドを順次実行する。最初のコマンドは
hSetBuffering stdin LineBuffering だが、おそらくこれは、今は無視した方がよいだろう (これはたまたまGHCでのみ必要であり、Hugsではこの記述が無くても動作する)。GHCでこれが必要なのは、GHCは入力を読み込むときかなり大きなブロック単位で読もうとするからだ。このブロックがいっぱいになるほど長い名前の人はどこにもいない。そのため、標準入力から読み込もうとすると、GHCはブロック全部にデータが入るまで待ち状態になってしまう。ここではこの機能を無効にするために、ブロックバッファの代わりに LineBuffering を使用するよう指定している。次のコマンドは
putStrLn で、画面上に文字列を出力する。9行目では "name <- getLine" と記述している。"name = getLine" と書くのがふつうだが、等号の代わりに矢印を使うと getLine が本当の関数ではなく、異なる値を返すこととがあることを表している。このコマンドは、「アクション getLine を実行し、その結果を name に格納せよ」という意味になる。最後の行は、直前の行で読み出した内容から文字列を生成し、画面上に表示している。
本当の関数ではない例をもう1つ挙げると、乱数値を返すものがある。ここでは、その関数を
randomRIO と呼ぶことにする。これを使えば「番号当て」プログラムを書くことができる。以下のコードを "Guess.hs" に入力する。module Main
where
import IO
import Random
main = do
hSetBuffering stdin LineBuffering
num <- randomRIO (1::Int, 100)
putStrLn "I’m thinking of a number between 1 and 100"
doGuessing num
doGuessing num = do
putStrLn "Enter your guess:"
guess <- getLine
let guessNum = read guess
if guessNum < num
then do putStrLn "Too low!"
doGuessing num
else if read guess > num
then do putStrLn "Too high!"
doGuessing num
else do putStrLn "You Win!"このコードを試してみよう。5行目で "import Random" と記述して、コンパイラに乱数関連の関数を使おうとしていることを伝えている (Preludeに乱数関数は組み込まれていない)。
mainの最初の行で、(1, 100) の範囲の乱数を尋ねている。浮動小数点や他の数値ではなく、ここでは整数値を使用しようとしていることをコンパイラに伝えるために、::Int と書く必要がある。これについての詳細は、第4章で述べる。次の行で、ユーザーに何が起きているかを伝え、main の最後の行でコマンド doGuessing を実行するようコンパイラに伝えている。関数
doGuessing は、ユーザーが推測した数を引数に取る。まず、ユーザーに推測するよう尋ね、キーボードから推測した値 (これは String である) を受け取る。まず、if文で推測した値が小さ過ぎないか確認する。ただし、guess が文字列で num が整数なので、まず guess を read して整数値に変換する必要がある。read guessは単なる純粋な関数なので (そして、IO操作も行わないので)、<- と記述する必要はない (実際のところ、そのように記述することもできない)。ここでは単純に、値をguessNumに代入する。do記述の中では、letに対するinは必要ない。入力した値が小さ過ぎた場合はメッセージを表示し、ふたたび
doGuessing を開始する。そうでなければ、入力した値が大き過ぎないか調べる。大き過ぎる場合はメッセージを表示し、ふたたび doGuessing を開始する。そうでなければ、小さ過ぎもせず、大き過ぎもしない数値を入力したことになり、値が正しかったはずである。そこで、正解したことを表示して終了する。実際には、それ以降コマンドが存在しないことにより暗黙のうちに終了している。明示的に return () 文を記述する必要はない。このコマンドはコンパイルすることもできるし、インタープリタにロードすることもできる。以下のようになる。
Main> main
I’m thinking of a number between 1 and 100
Enter your guess:
50
Too low!
Enter your guess:
75
Too low!
Enter your guess:
85
Too high!
Enter your guess:
80
Too high!
Enter your guess:
78
Too low!
Enter your guess:
79
You Win!ここで取り上げた再帰的なアクションは、実のところ、値を返してどこかで使うようなことはしていない。返り値を使う場合、コマンドを「陽に」記述するのは正しくない。ここでは、まず正しくない例を取り上げ、それが正しくない理由を説明し、そして正しい例を取り上げる。
たとえば、ユーザーに対して繰り返しいくつかの単語を入力するように求める単純なプログラムを書いているとしよう。ユーザーが空の単語を入力した場合 (つまり、何も入力せずにENTERキーを押した場合)、プログラムはそれまでにユーザーが入力したものをすべて出力して終了する。このプログラムの中心となる関数 (実際にはアクション) は、ユーザーに単語の入力を求め、それが空かどうかを確かめ、続行または終了するものである。正しくない実装例は、たとえば以下のようなものだ。
askForWords = do
putStrLn "Please enter a word:"
word <- getLine
if word == ""
then return []
else return (word : askForWords)この先に読み進める前に、このコードのどこが間違っているのかわかるかどうか試してみること。
誤りは最後の行、
word : askForWords のところにある。(:) を使うと、要素 (この場合は word) と別のリスト (この場合は askForWords) からリストを作ることを思い出してほしい。ところが、askForWords はリストではなく、実行するとリストを生成するアクションである。つまり、先頭に何かを追加する前に、アクションを実行して結果を得ておく必要がある。この例では、以下のようにしたいのだ。askForWords = do
putStrLn "Please enter a word:"
word <- getLine
if word == ""
then return []
else do
rest <- askForWords
return (word : rest)ここでは、まず
askForWords を実行しでその結果を変数 rest に格納している。その後で、word と rest から生成されるリストを返している。これで、単純な関数を書いてコンパイルし、関数とプログラムを対話環境で試し、リストを操作する方法をよく理解できたことと思う。
演習
演習 3.10 ユーザーが0を入力するまで繰り返し数字を尋ね、0が入力されたら数字すべての合計と積、および各数の階数値を出力するプログラムを書け。たとえば、一連の流れは以下のようになる。
ヒント:番号を読み出し、もしそれが0なら空のリストを返すIOアクションを書く。0なら自分自身を再帰的に呼び出して読み出した数値と再帰呼び出しの結果からリストを作成する。
Give me a number (or 0 to stop):
5
Give me a number (or 0 to stop):
8
Give me a number (or 0 to stop):
2
Give me a number (or 0 to stop):
0
The sum is 15
The product is 80
5 factorial is 120
8 factorial is 40320
2 factorial is 2ヒント:番号を読み出し、もしそれが0なら空のリストを返すIOアクションを書く。0なら自分自身を再帰的に呼び出して読み出した数値と再帰呼び出しの結果からリストを作成する。
前ページ「3.7 再帰呼び出し」
次ページ「4 型の基礎」
1 はじめに
2 スタートガイド
4 型の基礎
5 Basic Input/Output
6 Modules
7 Advanced Features
8 Advanced Types
9 Monads
10 Advanced Techniques
A Brief Complexity Theory
B Recursion and Induction
C Solutions To Exercises
これは、Haskell (ハスケル) のチュートリアル "Yet Another Haskell Tutorial" を日本語に翻訳したものです。
オリジナルのドキュメントは、
http://www.cs.utah.edu/~hal/docs/daume02yaht.pdf
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。
オリジナルのドキュメントは、
http://www.cs.utah.edu/~hal/docs/daume02yaht.pdf
などから入手できます。
日本語訳に関するご指摘は、コメントとしてお寄せください。
【Yet Another Haskell Tutorialの最新記事】

