変数に値を「代入」できない!?
Haskellの大きな特徴のひとつは、見出しのとおり、変数に値を代入できないことです。 「そんなバカな!そんなの変数じゃねえよ!」と思いたくなりますね。 厳密にいえば、変数を「初期化」することはできますが、 変数に値を「再代入」することができないのです。 つまり「変数」といいながら値は「不変(immutable)」なのです。 その意味では、「定数」と呼ぶほうがしっくりくるかもしれませんね。
しかし、どうしてわざわざそんな不便な仕様になっているのでしょうか。 いつでも変数の値を変えられるほうがよっぽど便利だし、 そもそもそんなことでプログラムが作れるのか?と疑問に思いますよね。
これはHaskellが「純粋関数型言語」たるための欠かせない要素なのです。 しかし、その説明はもっと後の段階ですることにします。
ひとまずは、コードを読みやすくするためという風に考えておいて差し支えありません。
例えば手続き型の言語で書かれたコードの中で、変数xが定義されていたとします。
すると、それが初期化の地点からいくらか離れたところで参照されるとき、
xに初期値がそのまま入っているかどうかはコードを追ってみないとわかりませんね。
どこで値が変更されているか分かったものではないからです。
しかしこれが「不変」の変数であれば変更される心配が全くありませんから、
いつどこでxを参照しても値は同じということが保証されているわけです。
// C言語
void func(void)
{
int x = 7; // xに7を代入
... // 様々な処理
printf("%d", x); // ホントに7が出力される?
}
-- Haskell
main = do
let x = 7 -- xに7を代入
... -- 様々な処理
putStr x -- 必ず7が出力される!
さて、そろそろ疑問がわいてきます。
「あれ?変数の値が変えられなかったら、どうやって繰り返しの構文を書くんだ?」
ここで衝撃の事実が発覚します。
for文が無い!?
さて、大変な事になりました。
変数の値が変えられないので、
普通の手続き型言語でいうfor文にあたる構文がHaskellには存在しないのです。
while文やdo-while文もありません。これらも結局、目標の変数がいくつになったか、
ということを繰り返しの終了条件にしているのですから。
困ってしまいました。世の中に繰り返しの入っていないプログラムなんてほとんど存在しません。 「繰り返し」というのはコンピュータに欠かせない動作です。 これがあるからこそコンピュータに仕事を任せられるのであって、 もし順次構造だけだったら、コードを書く時間で手計算したほうが絶対に速いはずです。
ではこれはHaskellの重大な欠陥なのでしょうか...?
いいえ、実はfor文やwhile文がなくても繰り返しを実装することができるのです!
お気づきかもしれませんね。そう、「再帰」を利用して繰り返しを実現するのです! 「再帰」という概念は手続き型言語にも存在していて、ご存じの方も多いと思います。 ここでもほとんど同じ意味で使っているといってよいでしょう。
「再帰ってなんですか?」という方もいると思いますので、おさらいしておきましょう。
//C言語
//普通の繰り返し
int ONEtoN(int N)
{
int sum = 0;
for (int i = 1; i <= N; ++i)
sum += i;
return sum;
}
//再帰関数
int ONEtoN(int N)
{
if (N == 1)
return 1;
else
return N + ONEtoN(N - 1);
}
この関数はいずれも1からNまでの自然数の和を返却する関数です。
2つ目の関数は、自分自身を呼び出していますね。
このように関数が自分自身を呼び出す構造を「再帰」といいます。
ここではfor文などの繰り返し構文は使われていませんが、
関数が何度も何度も呼び出されることで繰り返しを実現しています。
関数が呼び出されるたびに引数のNは1ずつ減っていきますね。
Nが1になったらそれ以上関数の呼び出しは行われず、
そこからは関数がどんどん呼び出し元に値を返していきます。
さて、今回はHaskellをはじめとする関数型言語がもつちょっと変な特徴を 紹介しました。 次回はついに、関数型言語の主役である「関数」について学んでいきましょう。 それではお楽しみに!