48時間でSchemeを書こう/最初の一歩

出典: フリー教科書『ウィキブックス(Wikibooks)』
移動先: 案内検索

まず、GHCをインストールしなければなりません。Linuxでは大抵もうインストール済かパッケージ・マネージャ(apt-getやyumなど)を通じて手に入ります。http://www.haskell.org/ghc/ からダウンロードすることもできます。バイナリパッケージをインストールするのが恐らく簡単でしょう。このチュートリアルはLinuxで開発されましたが、あなたがコマンドラインの使い方を知ってさえいればWindowsでも動くはずです。

UNIX(またはWindows Emacs)ユーザーには、非常によくできたEmacs modeがあり、syntax highlightingや自動インデントなどをやってくれます。Windowsユーザーはメモ帳や他のどんなテキストエディタを使うことができます。Haskellの文法は総じてメモ帳にやさしいですが、インデントには気を付けなければなりません。Eclipseユーザはeclipsefpプラグインを試してみるとよいかもしれません。最後に、GHCを使ったVisual StudioのHaskellプラグインもあります。

さあ、最初のHaskellプログラムを書く段になりました。このプログラムはコマンドラインから名前を読み込み挨拶を表示します。.hsで終わる名前のファイルを作って次のコードを打ち込んでください。正確にインデントするように気を付けてください。インデントを間違うとコンパイルできないかもしれません。

module Main where
import System.Environment

main :: IO ()
main = do
    args <- getArgs
    putStrLn ("Hello, " ++ args !! 0)

ではこのコードを見て行きましょう。最初の2行は我々がSystemモジュールをインポートするMainという名前のモジュールを作るということを指定しています。全てのHaskellプログラムはMainモジュールの中のmainというアクションから始まります。Mainモジュールは他のモジュールをインポートしても構いませんが、コンパイラが実行ファイルを作るために必ずなければなりません。Haskellはcase-sensitiveです。モジュール名は必ず大文字から始まり、定義は必ず小文字から始まります。

main :: IO ()という行は型宣言で、mainIO ()という型を持つことを示しています。IO ()はユニット型の値を持ち回るIOアクションで、ユニット型は()と表記される唯一の値を持つ、つまり何の情報も持ちません。Haskellでは型宣言は省略可能です。コンパイラが自動的に推論し、あなたが指定した型と違った時だけ文句を言います。このチュートリアルでは、明確さのため、全ての型を明記します。

IO型はモナドと呼ばれるものの一つです。モナドとは簡単な概念に難しい名前がついているだけです。基本的には、モナドは「私たちはある決まったやり方でいくらかの追加情報とともに値を持ち回り組合せますが、殆どの関数はそれについて気にしなくていいですよ」と言っています。どのようにこの追加情報を持ち回り、値を組合せるかがそのモナドをそれ自身たらしめるものです。モナドの持つ値はそれのまとう追加情報に関係無く普通の関数によって変えられたり、ある型から別の型に変換されたりするかもしれませんが、「パイプ」(値伝播機構)は同じままです。

この例では、「追加情報」は持ち回られる値を使って行われるIOアクションで、最終的な値は()です。IO [String]IO ()はどちらも同じIOモナド型に属し、異なる基本型を持っています。それが意味するのは、それらがそれぞれ[String]()の値を持ち回り、それに対して働くIOアクションだということです。このように基本型を包みこんでいるモナドの値は一般的に「アクション」と呼ばれます。IOモナドを、それぞれが外界に影響しながら、場合によっては持ち回される値に基いて働くことのできる動作(アクション)の連続として捉えるのが一番簡単だからです。

Haskellは関数型言語なので、プログラムを書くときは、コンピュータに実行すべき命令列を伝える代わりに、必要となるであろう全ての関数の定義を与えます。これらの定義はアクションと関数の様々な組合せを使います。コンパイラはそれら全てを纏め上げ、どのように実行すればいいかを探り出します。

定義は等式の形で書かれます。等式の左辺には名前と、変数を束縛する一つ以上の省略可能なパターンを書きます。右辺には他の定義の組み合わせを書き、コンピュータが左辺のパターンに出会した時に何をすべきかを定めます。このような等式はちょうど数学における普通の等式のように働きます。プログラムの中で左辺のパターンは右辺の式に置換可能であり、同じ値を得ることができます。これは「参照透過性」と呼ばれ、この特性は他の言語で書かれたプログラムに比べてHaskellプログラムが何をするのか判断するのを大分楽にしてくれます。

では、我々のmainアクションはどのように定義すればよいのでしょうか?IO ()アクションでなければならないのはわかっています。我々はコマンドライン引数を読み、出力を表示して、()を得たいのです。

IOアクションを作るには2つのやり方があります。

  1. return関数を使って普通の値をIOモナドに持ちあげる
  2. 2つの既にあるIOアクションを組み合わせる

今は2つのことをやりたいので、2番目の方法を用いましょう。組込みアクションのgetArgsはコマンドライン引数を読み込み文字列のリストとして渡します。組み込みアクションのputStrLnは文字列を取りコンソールにその文字列を表示するというアクションを作ります。

これらのアクションを組み合わせるには、doブロックを使います。doブロックはdoに続くインデントの揃った行の列で成ります。それぞれの行は2つの形のどちらかです。

  1. name <- action1
  2. action2

最初の形はaction1の結果を後続のアクションで使えるようnameに束縛します。例えば、もしaction1の型がIO [String](getArgsのように文字列のリストを返すIOアクション)なら、nameは"bind"演算子>>=によって後続の全てのアクションで持ち回される文字列のリストに束縛されます。二つ目の形はただaction2を実行し、もしあれば次の行に>>演算子を使ってそれを渡します。bind演算子はそれぞれのモナドで異なる意味を持ちます。IOモナドでは、それはアクションの結果である外的な副作用を働きながらアクションを順番に実行します。bind演算子の意味付けは特定のモナドに依存するので、一つのdoブロックの中に異なるモナド型の動作を混在させることはできません。この場合なら、IOモナドのみ使用可能です。

もちろん、これらアクションはそれ自身関数や他の複雑な式を呼び出し、(return関数を呼ぶかそれを呼ぶ関数を呼び出すことで)その値を受け渡していくことができます。この例では、まず引数リストの最初の要素(インデックス0番目、args !! 0)を取り、文字列"Hello, "の末尾にそれを繋げ("Hello, " ++)、そして最後にその結果をputStrLnに渡すと、doブロックのパイプに組み込まれる新しいIOアクションが返されます。

以上のように作られた新しいアクションは、型IO ()を持つ識別子mainにしまわれます。Haskell処理系はこの定義を認識し、その中のアクションを実行します。

Haskellに於て文字列は文字のリストなので、リストに対して可能な操作は全て文字列にも適用することができます。標準の演算子とその優先順位の完全な表は以下です。

演算子 優先順位 結合性 説明
. 9 関数合成
!! 9 リストのインデックスによるアクセス
^, ^^, ** 8 冪乗(整数、有理数、浮動小数点数)
*, / 7 掛け算、割り算
+, - 6 足し算、引き算
: 5 コンス(リストの構築)
++ 5 リストの結合
`elem`, `notElem` 4 リストの要素検証
==, /=, <, <=, >=, > 4 等号、不等号
&& 3 論理積
|| 2 論理和
>>, >>= 1 モナド束縛、モナド束縛(次の関数に値を渡す)
=<< 1 逆モナド束縛(上と同じだが、引数が逆)
$ 0 中置関数適用(f $ xf xと同じだが、右結合性を持つ)

プログラムをコンパイルし実行するには、以下のようなものを試してみてください。

% ghc -o hello_you --make listing2.hs
% ./hello_you Jonathan
Hello, Jonathan

-oオプションには作りたい実行ファイルの名前を与え、後はただ単にHaskellソースファイルの名前を指定します。

練習問題1

  1. コマンドラインから2つの引数を取り、その両方を使ってメッセージを出力するようにプログラムを変更しなさい。
  2. 2つの引数に対して簡単な算術演算を行うようにプログラムを変更しなさい。文字列を数値に変換するにはreadが、数値を文字列に戻すにはshowが使えます。いくつか違う演算を行うよう試してみてください。
  3. getLineはコンソールから一行読み込み、文字列として返すIOアクションです。名前を入力を促し、名前を読み、コマンドライン引数の代わりにそれを出力するようにプログラムを変更しなさい。