関数プログラミングのアプローチ (3)

関数的な乱数生成器

前回の課題は、乱数生成器を関数的に記述するにはどうすればよいかということでした。グローバル変数に乱数生成器の状態を保持することなく乱数生成器を書くにはどうすればよいでしょうか?
答えは、全ての状態を関数の引数として受け渡すということです。

class RandState:
    def __init__ (self,x,y,z,w,t):
        self.x, self.y, self.z, self.w, self.t = x, y, z, w, t

    def new ():
        return RandState(123456789, 362436069, 521288629, 88675123, 1)
    new = staticmethod (new)

def rand (r):
    t=(r.x^(r.x<<11)) & 0xffffffff
    x=r.y & 0xffffffff
    y=r.z & 0xffffffff
    z=r.w & 0xffffffff
    w=(r.w^(r.w>>19))^(t^(t>>8)) & 0xffffffff
    return w, RandState(x,y,z,w,t)

状態変数の数が多いので、まとめて取り扱うためにオブジェクトを作成しています。これを使うと、次のように乱数を生成することができます。

>>> r = RandState.new()
>>> rand(r)
(3701687786L, <__main__.RandState instance at 0xb7d885ac>)
>>> i,r = rand(r)
>>> i
3701687786L
>>> i,r = rand(r)
>>> i
458299110L
>>> i,r = rand(r)
>>> i
2500872618L

注目すべきは、rand()を呼ぶたびに毎回新しい状態オブジェクトを作りなおしているところです。そして、作りなおしたオブジェクトを生成した乱数といっしょに返値として返しています。次の乱数は、この新しい状態オブジェクトをrand()に渡すことで得ることができます。
このように新しい状態オブジェクトを作って返すことで、同じ状態オブジェクトを渡したら同じ結果が返ってくるという参照透明性の制約を満たした乱数生成器を作成することができます。(毎回新しいオブジェクトを生成する方法は非効率に思えますし、実際非効率であることもあります*1。この件については後ほど考察します。)
ところで、上の例では、変数iとrに対して代入を行っているように見えますが、これは参照透明性の制約に違反しないのでしょうか?
この問題はややややこしい点ですが、このケースに関しては、違反していないと言えます。これは、次のようなスコープのルールが仮定されていると考えることができるからです。

    -----------------------
>>> | r = RandState.new() |
    ---------             |
>>> | i,r = | rand(r)     |
    |       ---------------
    |                     | 
    ---------             |
>>> | i,r = | rand(r)     |
    |       ---------------
    |                     |
    ---------             |
>>> | i,r = | rand(r)     |
    |       ---------------

このように、'='のところで新しいスコープが作られていると考えることで、'='以前の変数i,rは手前のスコープの変数で隠されてしまって見えなくなっているだけで、実体としては別の変数として存在していると考えることができるため、参照透明性の制約を満たしていると考えることができるのです。

*1:それほど効率に影響を与えないということも多いですが