コンテンツにスキップ

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

出典: フリー教科書『ウィキブックス(Wikibooks)』

まず、GHCをインストールする必要があります。GNU/Linuxでは、多くの場合プリインストールされているか、パッケージマネージャー(例えば、ディストリビューションによってはapt、yum、pacmanがあります)を通じて入手できます。 https://www.haskell.org/ghc/ からダウンロードできます。バイナリパッケージが一番簡単ですが、自分が何をしているのか本当に理解している場合以外は、そうすることをお勧めします。このチュートリアルはGNU/Linuxで開発されましたが、コマンドラインの使い方を知っている限り、Windowsでも動作します。Macintoshでは、ターミナル内から動作します。

UNIX(またはWindowsのEmacs)ユーザー向けには、かなり優れたEmacsモードがあり、シンタックスハイライトや自動インデントが含まれます。Windowsユーザーはメモ帳などのテキストエディターを使用できます。Haskellの構文はメモ帳にとってかなりフレンドリーですが、インデントには注意が必要です。Eclipseユーザーはeclipsefpプラグインを試してみることができます。また、Visual Studio Codeには、GHCを使ったの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は大文字と小文字を区別します。モジュール名は必ず大文字で記述され、定義は必ず小文字で記述されます。 ず小文字から始まります。

main :: IO ()の行は、型宣言です。これは、mainIO ()の型であることを示しています。IO ()は、ユニット型()の値を持ちながら移動するIOアクションです。ユニット型は、1つの値しか許容しない型で、()と表記されます。つまり、情報を持たないことを表します。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アクションです。名前を入力を促し、名前を読み、コマンドライン引数の代わりにそれを出力するようにプログラムを変更しなさい。