Haskell:Dev Basics/Keyword
Haskellは静的型付け、型推測、並行性、遅延評価といった特徴を持ち、強力なリスト操作機能やパターンマッチ機構などをサポートする純粋関数型プログラミング言語。
Haskellは静的型付け、型推測、並行性、遅延評価といった特徴を持つ純粋関数型プログラミング言語。代表的な処理系としてはGHC(Glasgow Haskell Compiler)がある。
Haskellの特徴
Haskellは純粋関数型プログラミング言語である。純粋関数型プログラミング言語の大きな特徴の1つに「参照等価性が常に成り立つ」ことがある。こうした言語では、変数の値を一度設定(束縛)すれば、その値は変更されることがなく、同じ値を渡して関数を呼び出すと、常に同じ値が返される。こうしたことから「全ての関数が数学的な意味での関数である」ともいえる(Haskellの公式ページにはまさに「Every function in Haskell is a function in the mathematical sense」とある)。ある関数に特定の値を渡した場合の結果が常に同一であることからは、関数の評価は必要な時点まで遅延できる(遅延評価)という特性も得られる。
Haskellなど、関数型言語に分類される言語には「宣言的」という特徴もある。「宣言的」な言語では「問題を解決するための手順」ではなく「○○という関数とはどんなものであるか」を記述する。1つの例として階乗を求める関数/メソッドについて考えてみる。階乗とは次のように定義できる。
- 0の階乗を1とする
- 任意の正の整数nについて、nの階乗を「n×n−1の階乗」(1〜nの範囲にある数を乗算していった結果)とする
Haskellでは階乗を求める関数は、上の定義とおおよそ等しい形で記述できる。
fact 0 = 1
fact x = x * fact (x - 1)
比較のため、C#で階乗を求める「わざとらしい」メソッドを書くと次のようになる。こちらのコードだと、ループを使用して、1〜nの整数を順番に乗算していくという手順(手続き/命令)を記述していることが感じられる(もちろん、再帰を利用してより簡潔に記述することも可能だ)。
// ループを使ったわざとらしいコード
public static int fact1(int n)
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
return result;
}
// ラムダ式と再帰を利用した簡潔な記述
public static int fact2(int n) => n == 0 ? 1 : n * fact1(n - 1);
このように、宣言的にコードを記述できることは、Haskell(をはじめとする関数型言語)の大きな特徴の1つといえる。この他、Haskellが持つ特徴としては上述したように静的型付けでありながら型推測を行うことで簡潔な記述が可能、遅延評価に加えて、強力なリスト操作機能、パターンマッチ、ガードなどもHaskellの特徴として挙げられる。
リスト操作とパターンマッチ
Haskellのリストは同じ型の要素を含むデータ構造となる。また、リストを取り扱うための多くの関数がHaskellでは提供されている。以下に例を示す(以下では、Haskellの処理系であるGHCに含まれる対話環境で実行した結果をコメントとして含めている)。
s = "insider.net"
nums = [1..10]
length s -- 出力結果: 11
length nums -- 出力結果: 10
sum nums -- 出力結果: 55
Haskellでは文字列は「文字のリスト」であるため、リストとして扱える。また、「..」を使用することで数値や文字の範囲を表せる。length関数はリストの要素数を、sum関数はリストの要素の総和を求める関数である。この他にも先頭から任意の数の要素を取り出すtake関数、先頭要素を取り出すhead、先頭以外の要素を取り出すtail関数、末尾要素を取り出すlast関数、末尾以外の要素を取り出すinit関数、リストの要素を反転するreverse関数、リストに指定した要素が含まれているかを調べるelem関数など、非常に多くの操作が用意されている。
また、リストの内包表記を使うことで、より柔軟にリストの要素を指定できる。以下に例を示す。
num = [ n | n <- [1..10]] -- 1〜10の範囲の整数のリスト
even = [ n | n <- num, mod n 2 == 0] -- そこから偶数だけを取り出したもの
num -- 出力結果: [1,2,3,4,5,6,7,8,9,10]
even -- 出力結果: [2,4,6,8,10]
最初の行は「num = [1..10]」と同じことだが、内包表記の基本構文が現れている。「|」記号の左側がリストの実際の要素となり、「|」記号の右側には基となるリストを「<-」を使って記述する。この場合は「n <- [1..10]」なので、1〜10の各要素について「nをそのまま」リストの要素とする。例えば、「[ n * 2 | n <- [1..10]]」とすれば、2〜20の範囲の偶数を要素とするリストとなる(あるいは「[2, 4..10]」のようにすることで2、4、6、8、10を要素とするリストを作成することもできる)。
次の行では「述語」と呼ばれるものを使っている。述語は「|」記号の右側で基となるリストの後にカンマで区切って記述して、基となるリストからリストに含める要素をフィルタリングするのに使用する。ここでは「mod n 2 == 0」としているので偶数のみがリストの要素となる。なお、この例を見ると分かる通り、Haskellでは基本的に関数呼び出しは「関数名 引数 引数 ……」のように記述する。ただし、バッククオートを使って「n `mod` 2」のように記述することも可能だ。
リストの要素数はlength関数で求められるが、これを自分でcount関数として実装してみよう。ただし、その前に関数の基本的な構文を以下に示す。2つの数を加算するplus関数である。
plus x y = x + y
見れば分かるように、関数名に続けて引数リストを連ねて、「=」記号の後に関数の本体を記述する。
では、リストの要素数を調べるcount関数の定義を以下に示す。
count [] = 0
count (h:t) = 1 + count t
上で見たplus関数とは異なるように見えるが、これもれっきとした関数定義である。引数の部分で使われているのが「パターンマッチ」と呼ばれる機構である。count関数呼び出し時には、実際の引数と関数定義で記述されたパターンとのマッチングが行われる(なお、最初に掲載したfact関数でもパターンマッチが使われている)。
例えば、空のリスト([])を伴ってcount関数が呼び出されれば、「count [] = 0」にマッチする(その結果はもちろん「0」となる)。そうでない場合は、「count (h:t)」の方にマッチする。このとき、リストの先頭要素が「h」に、それ以外の要素からなるリストがtに束縛される。よって、ここでは「1 + count t」と再帰呼び出しをすることで、「先頭要素を取り除いたリストの長さ(count t)+1」をリストの要素数を計算している。リストの長さが有限であれば(Haskellでは無限長のリストも作成できる)、最終的には「t」が空のリストとなるので、そこで再帰呼び出しが完了して、最終的な結果が得られるようになっている。このように、Haskellでは再帰的な関数呼び出しがよく使われる。
なお、このコードは例えばcount.hsファイルなどに保存して、対話環境では「:l count.hs」を実行して読み込む必要がある。対話環境では単純に「count [] = 0; count (h:t) = 1 + count t」のようにセミコロンで区切って1行で実行するか、「:{」と「:}」を使って対話環境で複数行の入力を行う。
パターンマッチが上から順に行われる点には注意が必要だ。上の例では問題はないが、例えば、次のような関数では思った通りの動作とならない。
func x = print x
func 100 = print "a hundred!"
ここでは100を渡したときには「a hundred!」と画面に出力をしたいのだが、実際には「func x」の方にマッチングしてしまう。思った通りの動作にしたければ、2つの行の順序を入れ替える必要がある。
カリー化
最後にカリー化について簡単に触れておこう。カリー化とは「複数の引数を持つ関数を、単一の引数を持つ関数が連なったものとして表現する」ことといえる。このとき、単一の引数を持つ関数は「残りの引数を受け取って関数呼び出しの結果を返す関数を返す」ことになる。分かりにくいが先ほどのplus関数を例に取って考えてみる。
plus x y = x + y
この関数の型(シグネチャ)は実際には「plus :: Num a => a -> a -> a」と書ける。これはNumという「型クラス」に分類される任意の数値型aの値を受け取り、「型aの値を受け取って型aの値を返す関数を返す」という意味だ。「a」には例えば「Int」などの型が該当する。ここではplus関数に整数(Int)値を渡すものとして考えてみると、以下のようなことが可能である。
oneAdder = plus 1
oneAdder 2 -- 出力結果: 3
「plus 1」は「Intを受け取り(y)、それに1(x)を加算した関数」を返す。よって、「oneAdder 2」の呼び出し結果は「3」となる。また、このようにもともとの関数に一部の引数だけを渡すことで、その動作を部分的に決定することを「部分適用」などと呼ぶ。Haskellでは全ての関数はカリー化され、引数を1つだけ持つようになっている。
対話環境では「:t」コマンドで実際の型を調べられるので、参考までに以下に示す。
plus x y = x + y
:t plus -- 出力結果: plus :: Num a => a -> a -> a
oneAdder = plus 1
:t oneAdder -- 出力結果: oneAdder :: Num a => a -> a
本稿では、Haskellの特徴の中からほんの基本的な部分だけを取り上げた。Haskellを特徴付ける要素は他にもたくさんある。興味のある方は以下のリンクをたどったり、処理系をインストールして実際にコードを書いてみよう。
参考資料
- Haskell Language: Haskellの公式サイト
- Haskell 超入門: 基本構文をコンパクトにまとめた記事
- ウォークスルー Haskell: 入門者向けにHaskellの文法を説明
Copyright© Digital Advantage Corp. All Rights Reserved.