近年の実用に耐えるプログラミング言語には、構造体、クラスや ADT などのデータの塊(以後、単に「オブジェクト」と言う)を定義する機能が備わっています。
そういった言語でオブジェクトの構造を設計するとき、出来上がってきた設計について僕が真っ先に行う(おそらく最も、もっと言うなら唯一の)重要な sanity-check として、「その構造を持つオブジェクトoに仮に認識主体を認めた場合、oのどのプロパティがoの意思によって動きうる(生得的でない)のか1?そもそもoに認識主体を考えられるほどにoが『モノ』の形をしているか?o単体の意思によってプロパティが動きうる空間はどのような形をしているか?その一連の事情は設計・命名・コメントに反映できているか?」というものがあります。
ここで、「oに仮に認識主体を認めた場合」というのは、「もし、あなたがoそのものだったら」と読み替えて貰って構いません。頑張ってoになりきってください(冗談ではなく、大真面目に!)。
「プロパティが動きうる空間」というのもまた非常に伝え難い直感なのですが、概ねこのような意味で使っています。とある特定のoの状態が空間内の一点を成していると考えて、それら点を全て集めてくると2…ほら、空間っぽくないですか?この状態空間に実際に面白い具体的な位相的な構造を入れられるんだ~とか、そういう数学的な話をしたいわけではあんまりなくて(そもそもできるのか?)、ただ単に、点を集めてくると空間になるよね、という、そういう話をしています。
データモデルによっては有向グラフのような「流れ」があったり、もしくはもっと連続的な多様体上を(時間の離散化によって、オイラー法による数値解析のように離散的に動いているように見えているものの、『本来は』連続的に)動いているであったり、もしくはstratified automatonのように「状態が段階的に、地層のように分類される」ような構造が添加されているといったこともあるかもしれません。しかし、一旦、特定の数学的構造は考えないで良いはずです。想像ができる人なら、高次元多様体のようなものを考えてもらっても大丈夫です。僕の中のイメージはそのようなものです。しかし、例えばユークリッド空間との局所同相性がこの後の話に効いてくるかというと全くそんなことはないし、そもそも多分多様体じゃないので、そういうものを考えているんだなという気持ちだけ持っておいてくれれば十分です。
借用と憑依
さて、Rustの借用ルールというのは、
任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。 参照は常に有効でなければならない。
というものでした。
突然ですが、ファイバー(や、スレッドの「制御」)f が「変数への可変参照 &mut x を握る」というのを、ちょっと再解釈してみます。これを「fが、xに取り憑くことでxの意思というものを乗っ取り、(xの可視性が許す限りにおいて)好き勝手にxを変更できるようになる」と解釈するとどうでしょうか?
取り憑くというのは Mutex のロックを握るような行為であり(「取り憑かれたように」という形容詞は、まさに、「自分自身が制御できなくなったかのように、何かに突き動かされて」といった意味ですよね)、これはまさに(safe-)Rust の借用ルールの「可変参照は一つしか存在しえない」に対応しそうです。
「xの可視性が許す限りにおいて」、というのは、一見アクセス制御という言語機能特有の現象に見えるかもしれません。しかし、アクセス制御というものはそもそも、「ここを触れちゃうとマズいよね」みたいな利便性に基づいた機能というよりは、状態空間の形を規定し、(焼く前のパン生地の形を整えるように、あるいは焼成前の粘土に内側から力を加えて望んだ形の空間的閉塞を作る工程のように)成形するための機能であると考えるべきです3。ですから、先の断り文句というのはつまり、「xが論理的に到達しうる(ということにstructの定義によってされている)状態空間の中」という制約の中に限り、適当な近傍内の適当な点に飛びうるということが言われているに過ぎないわけです。
フィールドの変更をすることと、&mut selfを受け取るメソッドを呼び出すことは外部から見れば全く同じことですから(Scalaのdef x_=(x1: Int): Unitのようなメソッドを考えてみてください)、結局のところ、structが規定するような状態空間の中を動き回る、というのが、&mutを取ったときに許される行動になっているわけです(この点については、Rustのstruct+型クラスベースのプログラミングも、class-based OOPも同じでしょう。後者だと常に&mutが取られていてaliasingのチェックが無く、したがって、「弱い」型付けしか無いわけですが…)。
状態爆発と空間
以前、友人が「参照透過性が合理的で有用なことはわかるが、『状態』があることでどう困るのかがよくわかっていない」と言っているのを目にしたことがあります。この話題については僕もよくわかっていないままなのですが、その時は「状態はそれほど悪いわけではない、悪いのは状態爆発(state explosion; 状態空間が検査可能なサイズに収まらなくなること)だ」といった旨の受け答えをした記憶があります。
今思えば、別に状態爆発以外にも状態について困る点はいろいろあるわけで、今パッと考え付くもので言えば、状態が多くあると
- 広くなった状態空間の中でシステムの舵を取る必要があるが、「不正な状態に陥っているのか?それとも合法な状態なのか?」の判定がどんどん難しくなっていく
- キャッシュ一貫性を保つことが難しくなったり、data race が起こったりする
- aliasing などによって、処理Aからみて保証できていたと思っていた状態についての条件が別の処理Bからの状態の変更によって破壊され、Aが事後条件に違反してしまうようなことが起こる
などもありそうです。
状態爆発の話は、(1)の話が吸収しているかもしれません。というのも、実は「状態爆発は悪い」という話にはだいぶ大きい行間があって4、これはより丁寧に展開するならば、
- 前提として、『システム(ここで言うシステムは、アプリケーションプロセス内のインメモリの状態だけではなく、プロセスを取り巻く環境に浮かんでいる一連の計算機の群そのものを指す)を理想的な形で保守・運用・改善していくには、そのシステムが不正な5状態に入っているか、という条件が可能な限り明瞭に定義されており、また、その条件に当てはまるかどうかが可能な限り簡単に検証できる状態になっているべきだ』という考えがある。
- 状態爆発が「汚い」形で発生してしまうと、システム内の複数の個所が絡み合い、「不正な状態と正常な状態の境界」という部分空間6が、空間内で非常に複雑な形になってしまう。
- 逆に、「綺麗」な状態爆発というのは、すごく雑に言えば、一つの状態と別の状態が独立しており、つまり全体の状態空間が積空間のような形をしており、なおかつ「正常な状態」の部分空間も、積のような形になっている、といったものをここでは想定している
- 「密結合よりも疎結合が良い」として知られる経験則がまさに、この現象について言及しているのだと考えてよいと思う
- 従って、「汚い」状態爆発というのは悪い。
といった形の論理展開となっているはずなのです。結局のところ、ここで問題とされているのは、「不正」な状態を「正常」な状態と区別することの難しさであり、状態空間が十分綺麗な形にさえ保たれていれば、状態数自体が膨大であることはさほど問題ではなくなるように思えます。
読者によっては、ここで言っている「状態」が色々なものを指していることに気付いたかもしれません。(3)が話している「状態」は、メモリの中の値についての話になっています。一方、(1)は「システム全体が外部からのクエリに対する反応を変える根拠」(e.g. 管理下にあるデータベースの中に入っているデータ群)くらいの意味で状態、という語を使っていますし、(2)はシステムがどこかのレイヤで持っているデータ(e.g. CDN キャッシュ、インフラ管理ツールによって管理された状態、(1)や(3)の意味での状態)について話しています。前節では、主に(3)に着目した話をしていましたし、次節でもその話に戻ります。しかしながら、(1)や(2)の意味の「状態」についても、「状態がそこにあるべきか?」というのを考えるというプロセスについては、本稿の話がある程度効いてくるように思えます。
地に足をつけ、壁に手で触れる
最初にしていた話に戻りましょう。
&mut xをファイバーfが握るということはfがxに「取り憑く」ようなものであるという話を先ほどしました。ここで、さらに一歩外に踏み出して見てみれば、我々プログラマというのは、プログラムよりもさらに外側の世界に立った上で、プログラムを実行するファイバーたちを自分らの代理人(surrogate) として立てる存在であると考えることができるでしょう(ユースケース設計などでは、「アクター」がシステム内の操作を行っているのだという建前で考えることがありますが、実際のところ、プログラマたちはアクターの代理人として機能していて、プログラマたちの更なる代理人としてファイバーが存在していると捉えるのがより精密な理解であると考えています)。
この方向性で考えれば、プログラマたちは結局、間接的にオブジェクトたちに取り憑くことを究極的な目的としているのだと理解できるのではないでしょうか。であれば、その試みがうまくいくのかということを気にして、実行時に現れるオブジェクト達に事前に身を重ね、オブジェクトから見たsimulated realityが整合的であるかということに想いを馳せるというこの一連の行動は、さほど的外れではないものと思われます。
我々は、感覚器を通して世界の形状を知ることでその中を渡り歩いています。
オブジェクトに憑依することを想像しながらsimulated realityの様子を確かめるという行為はまさに、状態空間の「形」を直観を通じて読み取って、空間の中を渡り歩き、内なる世界の整合性を確かめるという、普段我々が現実世界で生きる際に行うことと同じような行為なのではないでしょうか。
-
プログラミングの文脈ではより普通に使われている言い回しで言えば、どのプロパティについて「
oが所有権を持っているのか」とも言えましょう。ただし、これは、例えばRustの型システム上でborrowとして表現されているかいないかという話では直接的にはなく、それよりももっと概念的なレベルで、オブジェクトがその性質についての制御を完全に適切に握っているのかというところを気にかけています(ほとんどの筋の良い設計で、それらは一致するはずです)。↩ - このように書くと、なんだか可能世界の話みたいでもありますね。↩
- これは、まさにOOPがencapsulationを通じて目指すことの一つであります。僕はencapsulationのことを、「宣言側で状態が動く範囲を制約する」ことそのものだと思っています。これは原義(例えば、Wikipediaの「カプセル化」の項に書いてあるような、「まとめたモジュールの内側の詳細を外側から隠蔽すること」といった意味)とは異なるかもしれませんが、現代のソフトウェア開発では原義の意味のようなことはもう当たり前になってしまった(はずな)ので、その一歩先を見たいわけです(そういう意味では、別の語を割り当てるべきかもしれないですね)。だから、僕の中では、可変状態を握るオブジェクトが不正状態に突入しないように内部実装をencapsulateするのも、Make Illegal States Unrepresentable原理も全く同じ話なのです。僕の中では、「実装を隠す」であるとか「フィールドの可視性を狭める」みたいな、よくencapsulationの目的とされること自体は心底どうでもよくて、だから、getter/setterを付けるだけでencapsulationをしたと言い張る言説や、クラスに物事を「まとめて」フィールドをprivateにすることそのものがencapsulationなのだ、といった主張に根幹から反対するのです。我々は、その制約によってどのように状態空間が削られ、または拡張され、あるいは圧力が掛かるのかというところを、そしてまさにそのことのみを気に掛けるべきなのではないでしょうか。↩
- 想定されるツッコミ:マイナス一行目とゼロ行目(0-indexed)の間のギャップを行間とは言わないだろ↩
- 一体何が「不正」なのであるか、という話をしだすと本当に長くなるので、ここでは、思い思いの(ナイーブでも良い)意味で「不正な状態」を捉えてもらって十分です。↩
- 位相の直観に絡めれば、これは、「少しでもつっつく(nudgeする)と不正な状態に突っ込んでいくような正常な状態」と「少しでもつっつくと正常な状態に戻る不正な状態」の和であるといえましょう。↩