読者です 読者をやめる 読者になる 読者になる

Haskell でバグの出にくいプログラミング (4) ローカルスコープによる変数代入の模倣

前回は、副作用である変数代入という概念が純粋関数型言語である Haskell には存在しないということを書きました。 今回と次回は、そのような変数という概念を純粋関数的に模倣した Haskell の State モナドについて書こうと思います。

今回は、State モナドの説明の前段階として、ローカルスコープを用いた変数代入の模倣についてです。

ローカルスコープの妙技

前回の記事で載せたコードをもう一度掲載します。

x = 3
main = print x
where
x = 100

このプログラムは、x の値を出力するというものですが、出力される値は where 節内で定義された x です。 一見すると、x という変数に値を代入しているようにも見えますが、実際は main 関数内で局所的に定義された別の x であり、 グローバルスコープがローカルスコープによって部分的に隠蔽されているだけで、グローバルスコープにある x の値は不変です。 当然ながら、このような動作は純粋関数的です。

このローカルスコープをうまく使うことで、変数を使っているように見せることができます。

Haskell Haskell のコードと等価な C++ のコード
hoge x =
let y = 3 + x
z = 2 * x in
let x = y + z in
2 * x
main =
print $ hoge 6
#include <iostream>
int hoge(const int x){
const int y = 3 + x;
const int z = 2 * x;
{
const int x = y + z;
return 2 * x;
}
}
int main(void){
std::cout << hoge(6) << std::endl;
return 0;
}

解りやすさのため、C++ のコードも併記しました。 Haskell のコードでは、ローカルスコープを定義できる let 〜 in ... 構文を使用しました。 それによって、x の値が上書きされているように見えるプログラムを組むことができます。

関数の引数で隠蔽

当然ながら、関数の引数もローカルスコープ内に入っているので、さきほどのプログラムを関数の組み合わせで書くことができます。

hoge x =
let bar x = 2 * x in
let foo y z = bar (y + z) in
foo (3 + x) (2 * x)
main =
print $ hoge 6

特に、Haskell では無名関数をλ式というもので作ることができるので、それを用いると関数名を省けて、手軽に書くことができます。

hoge x =
(\y z -> (\x -> 2 * x) (y + z)) (3 + x) (2 * x)
main =
print $ hoge 6

λ式は、(\引数リスト -> 処理) という風に定義されます。 前々述のコードと見比べてみると、分かりやすいかと思います。

ローカルスコープを抜けると過去だった

しかし、当然ながらローカルスコープを抜けてしまうと、代入されたように見えた値が元に戻ってしまいます。 そのため、変数代入をローカルスコープで模倣する場合、変更があった先の処理は全てそのローカルスコープ内にいれてやる必要があります さらに、変更がある度にλ式や let 構文を何回も使う必要があり、面倒ですし、読みにくそうです。

そこで、このようなローカルスコープによる変数代入を模倣しつつ、ローカルスコープから勝手に出れないようにする仕組みがあると便利です。 次回は、そのような仕組みを実際に定義し、State モナドの説明をしようと思います。

担当:齋藤 (刻々と状態の変化する現実世界はローカルスコープが無限に入れ子になっているのでいつかスタックオーバーフローする、と言ってみるテスト)