Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

副作用がある中において「どれとどれが同じものなのか」っていうのを考えるのは難しいという話

「関数型」云々のさい、大抵は副作用が如何に問題なのか、という話が出てくる。とはいえ、自分自身で「副作用がこれだけ問題なんですよ」ということが、議論の内容はわかるにしても、それが現実的にどうなのか、といった説明レベルになると、上手く説明できるかどうか、あまり自信が無い。そんなことを考えながら、『計算機プログラムの構造と解釈』(通称: SICP) を読んでいたら、哲学的な下の文章に出くわした。

参照透明性を一旦捨てると, 計算オブジェクトが「同じ」であるとは何を意味するかの概念を形式的に捕らえるのは難しくなる. 実はわれわれのプログラムがモデル化している実世界の「同じ」の意味もあまりはっきりしない. 一般に二つの見かけ上同じなオブジェクトが本当に「同じもの」であるかは, 一つのオブジェクトを変えてみて, もう一つのオブジェクトが同じように変っているかを見て決める. しかし「同じ」オブジェクトを二度観測し, オブジェクトのある性質が一回目と二回目の観測で違っているということ以外に, あるオブジェクトが「変った」ことがどうして分るだろうか. つまり何か先験的な 「同一」という概念なしに「変化」を決めることは出来ず, 変化の効果を観測することなしに同一性を決めることは出来ない.

この文章について、とても面白いなと思ったので、このことについて考えたことを、ここにメモしておく。

同じであることと、違うということ

参照透明というのは、この本に照らし合わせて考えれば「式の評価の結果を変えることなく、式の中で「等しいものは等しいもので置き換えられる」という概念の成り立つ言語」ということになっている。このように説明されると分かりにくいけれども、例えば以下のようなRubyのプログラムを書いてみる。

class TestReferent
  def initialize
    @n = 1
  end

  def not_referent x = 0
    @n += 1
    x + @n
  end

  def referent x
    x + 1
  end
end

ref = TestReferent.new
Random.rand(100).times { ref.not_referent }
puts ref.not_referent 1
puts ref.not_referent 2
puts ref.referent 1

このとき、referent メソッドは、何度呼び出されても、常にx + 1が得られる。しかし、not_referent メソッドの場合返ってくる値は、その呼び出された回数に依存する。なので、「等しいものは等しいもので置き換えられる」とは言い難い(ref.referent 1なら、簡単に2に置き換えることができる)。これが正しい参照透明の説明かどうかhわからないけれど、どういう雰囲気のものなのかは掴めるのではないかと思う。

このように、「何かを何かで置き換えられる」という性質を捨てたとき、実は「同じもの」であるという概念が、非常にあやふやになるということを示している。

例えば、常に自転車の鍵を青い箱に入れておくような習慣があるとして、では今も「同じように」青い箱の中に自転車の鍵が入っているかどうかは、観察してみなければわからない。もしかしたらうっかり机の上に置きっ放しにしていることだってある。

抽象的な話になってきたので、実例を出してみよう。下はRubyの例である:

class Hakai
  def initialize n
    @n = n
  end

  def destroy!
    @n.reverse!
  end
end

stupid = "esehara"
h = Hakai.new(stupid)
puts h.destroy!
puts stupid

このとき、puts stupidでは何が出力されるだろうか? Rubyの仕組みについて詳しい人であるならば、即答できるかもしれないけれど、自分はこの挙動を見たときに些か混乱した。この場合、hも文字列が反転してしまう。つまり、@nnは一緒であることが判明する。

また、この手の問題は配列においても発生する。例えば次のようなコードはどうだろう:

a = [1, 2, 3]
b = a
a[2] = "foo"
puts b

これらの挙動については、あまりにも基礎的であるかもしれないが、しかし同時に「変える」ということが、如何に「どこまで同じなのか」ということが不明確であり、また同時にそれに対して意識的でなければならないことを示している。これはLispについても無縁ではなく、『On Lisp』においては、nreverseという関数が紹介されている。

(defvar *foo* '(foo bar hoge fuga))
(format t "result exp: ~A~%" (nreverse *foo*))
(format t "result *foo*: ~A~%" *foo*)

このとき、*foo*’(foo)というリストに破壊されてしまっている。

このように、副作用が難しいのは、「何と何が同じなのか」ということが不明確であるということ。さらにいうならば、見かけは違うけれども「同じである」ものという厄介な問題も存在している。例えば、先ほどのRubyの配列に関するコードであるならば、bが意図しない結果を持っている場合、 aが悪さをしている可能性も同様に考えなければならないだろう。つまり、変化は伝播する。したがって、この変化がある場所から変化することを示しやすいHaskellのような言語もある(と思う……記憶違いでなければ)。

スタイルとしての「副作用をできるだけ使わない」とすること

もし「関数型」を使うことで、何か学べることがあるとするならば、上記のようなスタイルだとも言える。コンピューターは副作用の塊なので、それとなんらかの形で仲良くしなければならないので、ある程度妥協しなければならない。

しかし、果たしてどこまでが変化の起きるものなのか、ということを意識し、できるだけ変化させずにする方法を学ぶことは非常に有効なものであるという風に、自分は考える。ただ、このスタイルは少々オーバーヘッドなどの問題があったりする場合もあるけれども、これはまたパフォーマンスを気にしなければいけない問題ときに考えるべきことだとも言えると思う。