束縛について(初期化/代入/束縛)(Haskell)
この記事では束縛という機能について考えてみます。「後からその値を変更できない」という点において、Haskellの変数は命令型言語の変数と全く違う特徴を持っています。初期化/代入/束縛の3つの違いを意識しながら学んでいきましょう。
束縛とは?
そもそも束縛とは何でしょうか。束縛とは変数と値を結びつけることであり、後からその値を変更できない点に大きな特徴があります。要するに定数だけでコードを書くようなモノであり、思わぬミス・エラーを防ぐための仕組みとなっています。束縛を考えるにあたって、初期化/代入/束縛の3つの違いを考えてみます。
初期化:変数の宣言と同時に値を代入
代入:変数の宣言後に値を代入
束縛:変数と値を結びつける
「変数の値を操作する」という点では同じですが、これら3つは全く違った機能を持っています。
初期化とは?
まずは初期化からです。変数の宣言と同時に値を代入するモノであり、以下のnum1,num2はどちらも値を初期化しています。
int num1 = 10;
int num2 = num1;
num1は宣言と同時に10という値を代入していますし、num2も宣言と同時にnum1の値を代入しています。初期化は変数を宣言した際に一度しかできません。
代入とは?
次に代入についてです。代入は変数の宣言後に値を代入します。
int num1;
num1 = 10;
num1 = 20;
num1という変数をまず一度宣言して、そこから10,20という値を代入しています。初期化は一度しかできませんが、代入は何度でもできます。
破壊的代入/再代入
変数はよく「値を入れる箱」として箱として説明されることが多いのではないでしょうか。この箱には値を入れる回数に宣言もないので、一度値を代入した後に後からまた別の値を代入できます。これを特に破壊的代入・再代入とも呼んだりします。変数の値を書き換えるというのは状態を扱うことであり(代入は副作用と考えられ)、関数型言語の多くでは禁止されています。
束縛とは?
最後に束縛についてです。束縛とは変数と値を結びつけることであり、後からその値を変更することはできません。
num1 :: Int
num1 = 10
main = print $ num1
ここではnum1という変数を10に束縛しました。正確に言えば「変数を値に束縛する」のであって、「変数が値を束縛する」のではありません。変数は束縛される側となっています。
関数型言語:束縛
ここで注目したいのが束縛が関数型言語ならではの機能である点です。関数型言語には変数への代入がないか、もしくは非常に限定されています。(原則として)一度変数の値を決めたら変えられない束縛しかありません。代入を持つような関数型言語でも、代入と束縛は明確に区別して扱われます。
未初期化変数はない
Haskellでは変数は必ず束縛され、未初期化変数(=値が入っていない変数)というのは存在しません。例えばこちらを実行するとエラーとなります。
num :: Int
main = print $ num
注目したいのがエラー内容です。
The type signature for ‘num’ lacks an accompanying binding
となっており、「束縛されていないよ!」とメッセージが出ています。
(※)ちなみに変更できな変数のことをイミュータブル(immutable)であるとも呼びます。一口に変数と言ってもJavaやPythonの変数ではなく、数学的な変数のことを指しています。
束縛するメリット2つ
なぜHaskellでは変数の値を変更できないのでしょうか。それには大きく2つのメリットがあります。
1、値が勝手に変わってしまった!がなくなる
2、値が入っていない!がなくなる
(NullPointerExceptionエラーなど)
1、値が勝手に変わってしまった!がなくなる
今までの命令型言語では変数の値の書き換えが可能でした。便利な一方、ミス・エラーを生む原因となっていました。例えばifによる判定を考えてみます。aとbが同じ値かどうか?を判定する時に、それぞれの値が変更できるとしたらどうでしょうか。
int a = 10;
int b = 10;
:
.. a ..
.. .. b ..
:
if (a == b)
コードの1番最後の部分でif (a == b)
の判定を行なっていますが、途中でaとbの値が変わった可能性があります。なので変数の宣言をしてから全てのコードを追って、値が変更されていないかをチェックして、それからようやくa == bかどうか?をifで判定できます。「変数の値が書き換えられてないか?」に注目しながらコードを読むのはかなり大変ですよね。
・前後の部分もチェックしないといけない
・値を変更したら全体を調査しないといけない
・エラー発生時に調査が困難
「変数に注意を払いながらコードも読む」というのは頭の中でジャグリングをしているようなモノです。最も大切な「コードを書くこと」以外に注意を払わなければいけないので、開発をする際に大きなストレスとなります。
値が束縛されていた場合
しかし変数の値が束縛されていたらどうでしょうか。最初に初期化したaとbの値が変わることはありません。つまり途中のコードは全てすっ飛ばしてif (a == b)
の判定だけに注目ができます。
int a = 10;
int b = 10;
:
.. a ..
.. .. b ..
:
if (a == b)
「いっそのこと定数としてロックしてしまおう」というのが関数型言語の考えです。定数のみを扱うことにはたくさんのメリットがあります。
・テストが楽になる!
・バグ、エラーを減らせる!
・影響範囲が狭いので読むのが楽!
・後からカンタンに修正できる!
・「コードを書くこと」だけに集中できる!
「定数だけでコードを書く」というのは「人の注意力に頼らないための仕組み」と言えます。
2、値が入っていない!がなくなる
次に2つ目のメリットして、値が入っていないことがなくなる点です。変数へは常に値が入っていることになります。例えばHaskellでこちらを実行するとエラーになります。
num1 :: Int
num2 :: Int
num1 = 10
main = print $ num1
num1に対しては10を代入しましたが、num2には(うっかり忘れて)何も代入しませんでした。すると先ほどと同じようにエラーが出て「束縛されてないよ!」と教えてくれます。
The type signature for ‘num2’ lacks an accompanying binding
Javaでいうnullに相当する空の値を束縛することはできません。num :: Int
の変数には必ずInt型の値が入るようになりますし、String型の変数には必ず文字列が入るようになります。それでいて値を後から書き換えることはできません。
これによってNULLチェックのような、プログラムを作ることとはあまり関係がないことをする必要がなくなります。そもそもNULLのような空の値はあまり使い道がないので(あったとしても間違いを引き起こすことが多いので)初めから関数型言語では禁止されています。空の値を禁止する..ということによっても、人の注意に依らない堅牢性のあるプログラムを作れるようになるのです。
Discussion