厳密な光線追跡 - その16 [光線追跡エンジンを作る]
前回までで、光線追跡エンジンのオブジェクト設計をいちおう終えた。それほど複雑ではない。今日は僕なりのMathematicaのオブジェクト指向スタイルについて。
古典的なところでは古株MaederさんのClasses.mがある。完全にオブジェクト指向環境をMathematicaに構築するという発想で作られていて、なんというか濃いやりかたである。
また、こっちは、クラス定義の字面から予約語までJava風に統一されている。こういうのってすごいんだけど、どうも僕から見るとやっぱり「濃い」ように思えて、ここまでしなくてもいいんじゃないか、と感じてしまう。
日本的なアプローチとしては、例えばこの方はかなりまじめにMathematicaの上にオブジェクト指向環境を乗せておられる。
また、以前コメントいただいたこの方は、どちらかというと僕のやりたいことに近いことを考えておられる。この括弧が連続するスタイルは面白い。括弧が深くなってどれがなにの引数なのかわからなくなる、というのはMathematicaのコードが読みづらくなる原因のひとつで、僕も自分で書いたのをあとから読んでうんざりすることよがくある。
なんでそういう中途半端なアプローチにしたかというと、オブジェクト指向環境をMathematicaに構築したいわけではなく、今回の光線追跡エンジンの記述に便利なようにしたいからで、ようするに自分で書くためにCocoa/Objective-CやCore Foundationのスタイルに似せたいというだけである。
まず、ひとつのオブジェクト(インスタンス)をMathematicaのひとつのシンボルにわりあてる。オブジェクトにメッセージを投げるのを
オブジェクトの生成には、オブジェクトの型に従った専用の生成関数
オブジェクトの破棄には、ただ
重要なのはオブジェクトとして作られたMathematicaのシンボルが内部構造、つまりインスタンス変数を持つ、ということ。そしてそれが生成関数を使えば独立に複数個生成できる、ということ。
やはりイメージとしてはMac OS XのCore Foundationといっしょの感じ。
これで何がうれしいのか、というとbk7というオブジェクトは、例えば内部にキャッシュを持てる。光線追跡ではいろいろな媒質に対して同じ波長の屈折率を繰り返し使う。これを外部のキャッシュ用変数に保持するのではなくてbk7というオブジェクト自身が持っていて、同じ波長に対してはキャッシュされた値を返すようにする。こうするとbk7だけでなくfs11やfusedSilicaなんていう媒質オブジェクトはそれぞれにキャッシュ変数を自分で持てる。自分自身でキャッシュがヒットしたかどうかを判断して、ヒットすればキャッシュの値を返して、ヒットしなければ新しく計算してその値をキャッシュした上で返す、ということをする。
こういうキャッシュ動作は媒質オブジェクト共通の動作だけど、いちいち媒質定義をするたびに実装するのは面倒なので、共通する動作はクラスとしてまとめよう、ということ。ようするにキャッシュ動作はクラス定義に記述してあって、具体的なインスタンスは媒質の分散式だけを書けばいい、ということにしたい。
こういうキャッシュなんていう内部動作を、インスタンスとして生成するときに見えなくする、という方針はオブジェクト指向の考え方そのものであって、重要であると僕は思っている。
では、シンボルごとに独立したキャッシュをどうやって実装するか、というのはまた実装の段階で考えることにするけど、例えば単にこうするだけで
他にも、Mathematicaでは上向き割当てというのが使える。例えば
Set(=)とUpSet(^=、それと、右辺を実行時に評価するSetDelayedとUpSetDelayedの:=と^:=)とはほとんど単なる字面の違いしかない。マニュアルには
また、マニュアルにはないけどSetによる下向き割当てとUpSetによる上向き割当ては別々のデータベースになっていて一カ所に定義を溜めるよりふたつに分けたほうが効率もあがる、という利点もある。
とりあえずこういったメカニズムを使えばオブジェクト別のキャッシュは実装できる。
こういうやりかたでオブジェクトにしたいのは
ぐらい。
媒質オブジェクトは屈折率の値をキャッシュするのが一番の目的。面形状オブジェクトはそれが定義されたときに面の式を評価して数値計算に効率的な形にしておく(可能な場合はCompiledFunctionに変換しておく)。
boundaryオブジェクトは、同次行列とその逆行列を保持する。光学系オブジェクトは近軸マトリクスを計算して保持する。
ようするに数値計算上の効率を上げるための作業を、明示的ではなくオブジェクトが定義されたときにすませてしまうようにするためのものばかりである。
4.4.1 Mathematicaでオブジェクト指向
Mathematicaにオブジェクト指向の考え方を取り入れることは、すでにこれまでいろいろなアプローチがある。古典的なところでは古株MaederさんのClasses.mがある。完全にオブジェクト指向環境をMathematicaに構築するという発想で作られていて、なんというか濃いやりかたである。
また、こっちは、クラス定義の字面から予約語までJava風に統一されている。こういうのってすごいんだけど、どうも僕から見るとやっぱり「濃い」ように思えて、ここまでしなくてもいいんじゃないか、と感じてしまう。
日本的なアプローチとしては、例えばこの方はかなりまじめにMathematicaの上にオブジェクト指向環境を乗せておられる。
また、以前コメントいただいたこの方は、どちらかというと僕のやりたいことに近いことを考えておられる。この括弧が連続するスタイルは面白い。括弧が深くなってどれがなにの引数なのかわからなくなる、というのはMathematicaのコードが読みづらくなる原因のひとつで、僕も自分で書いたのをあとから読んでうんざりすることよがくある。
4.4.2 僕はどうしたいのか
僕はここではちょうどMac OS XのCore Foundationフレームワークがフレームワークのユーザ(プログラマ)にとってオブジェクト指向になってみえるように、そう言うスタイルでMathematicaのパッケージを使えるようにすることを目標にする。したがって実装の字面にはこだわらない。なんでそういう中途半端なアプローチにしたかというと、オブジェクト指向環境をMathematicaに構築したいわけではなく、今回の光線追跡エンジンの記述に便利なようにしたいからで、ようするに自分で書くためにCocoa/Objective-CやCore Foundationのスタイルに似せたいというだけである。
まず、ひとつのオブジェクト(インスタンス)をMathematicaのひとつのシンボルにわりあてる。オブジェクトにメッセージを投げるのを
object[message]というふうに普通の関数の形で書く。メッセージに引数があるときは
object[message][arg1, arg2,...]とする。普通の言語ではこういう関数の書き方は許されないことが多いけど、Mathemaitcaでは問題なく、このobject[message]が評価された結果、新しいHead(Mathematica式の頭部)になる、というのが実装上のミソ。まあ、あまり先走ってはいけない。
オブジェクトの生成には、オブジェクトの型に従った専用の生成関数
createAnObject[object_Symbol, arg1_, arg2_,...]を使う。これを呼ぶとobjectというシンボルに引数によって初期化されたオブジェクトが格納される。
オブジェクトの破棄には、ただ
ClearAll[object]あるいは
Remove[object]とするだけ。これで全部。継承やクラス定義の機能はない。
重要なのはオブジェクトとして作られたMathematicaのシンボルが内部構造、つまりインスタンス変数を持つ、ということ。そしてそれが生成関数を使えば独立に複数個生成できる、ということ。
やはりイメージとしてはMac OS XのCore Foundationといっしょの感じ。
4.4.3 そんなことをして何がうれしいのか
具体的に書くと例えば媒質オブジェクトはcreateMedium[bk7,refractiveIndexFunction]などとして作って
n=bk7[refractiveIndex][0.55]とすると波長0.55μmでの屈折率を返す、なんて言う感じ。
これで何がうれしいのか、というとbk7というオブジェクトは、例えば内部にキャッシュを持てる。光線追跡ではいろいろな媒質に対して同じ波長の屈折率を繰り返し使う。これを外部のキャッシュ用変数に保持するのではなくてbk7というオブジェクト自身が持っていて、同じ波長に対してはキャッシュされた値を返すようにする。こうするとbk7だけでなくfs11やfusedSilicaなんていう媒質オブジェクトはそれぞれにキャッシュ変数を自分で持てる。自分自身でキャッシュがヒットしたかどうかを判断して、ヒットすればキャッシュの値を返して、ヒットしなければ新しく計算してその値をキャッシュした上で返す、ということをする。
こういうキャッシュ動作は媒質オブジェクト共通の動作だけど、いちいち媒質定義をするたびに実装するのは面倒なので、共通する動作はクラスとしてまとめよう、ということ。ようするにキャッシュ動作はクラス定義に記述してあって、具体的なインスタンスは媒質の分散式だけを書けばいい、ということにしたい。
こういうキャッシュなんていう内部動作を、インスタンスとして生成するときに見えなくする、という方針はオブジェクト指向の考え方そのものであって、重要であると僕は思っている。
では、シンボルごとに独立したキャッシュをどうやって実装するか、というのはまた実装の段階で考えることにするけど、例えば単にこうするだけで
bk7[cacheValue]=0.518 sf5[cacheValue]=0.677とすればシンボルに結びつけた値を、シンボルごとに作ることができる(ただし、cacheValueというシンボルに割当がなければ。chacheValueなんていう名前にしたのはわかりやすさのためで、値の割り当てられていないシンボルならなんでもいい)。bk7というシンボルを消去すればこの値の割当ても消去される。
他にも、Mathematicaでは上向き割当てというのが使える。例えば
cacheValue[bk7]^=0.518 cacheValue[sf5]^=0.677これはMathematica内部では
UpSet[cacheValue[bk7],0.518] UpSet[cacheValue[sf5],0.677]としたことになるけど、こうすると、この値はcacheValueというシンボルではなく、引数に入ったbk7やsf5というシンボルのほうに割り当てられる。これはcacheValueというシンボルに割当がない限り呼び出すことができて、sf5というシンボルを消去すれば自動的にその値も消去される、ということになる。
Set(=)とUpSet(^=、それと、右辺を実行時に評価するSetDelayedとUpSetDelayedの:=と^:=)とはほとんど単なる字面の違いしかない。マニュアルには
Exp[g[x_]] ^:= expg[x]のような例があって、組み込み関数であるExpの動作を変更したいときなどに、Expにその定義を結びつけるより、この場合gに結びつけられたほうが自然である、というような話がある。もちろんExpにはいろいろな定義がはじめからなされていて、それにさらに定義を付け加えるとパターンマッチに時間がかかるなどの効率上の問題もある。
また、マニュアルにはないけどSetによる下向き割当てとUpSetによる上向き割当ては別々のデータベースになっていて一カ所に定義を溜めるよりふたつに分けたほうが効率もあがる、という利点もある。
とりあえずこういったメカニズムを使えばオブジェクト別のキャッシュは実装できる。
こういうやりかたでオブジェクトにしたいのは
オブジェクト | 内容 |
medium | 媒質 |
surface | 面形状 |
boundary | 形状以外の面の情報 |
sequenceOfMedia | 光学系全体 |
媒質オブジェクトは屈折率の値をキャッシュするのが一番の目的。面形状オブジェクトはそれが定義されたときに面の式を評価して数値計算に効率的な形にしておく(可能な場合はCompiledFunctionに変換しておく)。
boundaryオブジェクトは、同次行列とその逆行列を保持する。光学系オブジェクトは近軸マトリクスを計算して保持する。
ようするに数値計算上の効率を上げるための作業を、明示的ではなくオブジェクトが定義されたときにすませてしまうようにするためのものばかりである。
2012-11-25 22:15
nice!(0)
コメント(2)
トラックバック(0)
今回の話のような(あるいは、なんちゃってMathematicaで書かれていたような)「シンボルなどがどのようなスコープでどのように格納されるか(そして、それを利用してどんなことができるか)」ということは、非常に参考&勉強になります。ありがとうございます。
by jun hirabayashi (2012-11-26 19:46)
コメントありがとうございます。
「参考になる」と言っていただけるとは光栄です。こちらこそ勉強させてもらっています。http://www.hirax.net/dekirukana10/mathematica/
を読ませていただいて、これがあれば「なんちゃってMathematica」っていらないじゃん、と思ってしまいました...
こっちの光線追跡エンジンのほうは、もう少し煮詰まったら実装に入りますので、そのときはまた突っ込んでいただけるとありがたいです。
よろしくお願いします。
by decafish (2012-11-26 20:28)