標高+1m

Don't be rational.

場を使った相対論的オブジェクト間コミュニケーション

アクターやオブジェクトにおいて採用されているメッセージング機構(つまり宛先アドレスを持ったメッセージ)は、自然界のシミュレーションやIoTデバイス間コミュニケーションに適さないのではないか、という仮説のもと、メッセージングに依らないオブジェクト間コミュニケーションについて考えています。ここまでの経緯は、以下2つの記事を参照してください。

VOC (Vision Oriented Communication)の中心にあるコミュニケーション機構は、以下の2点に集約できる。

  • 情報は自身を中心に波状に拡散する
  • だれでも視野の中にある情報は読める

f:id:ympbyc:20151113195651p:plain

前回は、場(場 - Wikipedia)を使った機構を紹介し、同時にこの方法のパフォーマンス上の問題点について示唆した。現状の問題点は以下の3点。

  • 全てのオブジェクトが絶対座標を持っていてかっこ悪い。
  • 場を愚直にグリッドとして実装すると、大量の無駄な計算が生じる。
  • 情報が1フレームで場全体に拡散されるのは、光速を超えた情報伝達であり、相対論に違反している。

今回はこの3点を解決する実装を紹介する。

座標を廃止し、各オブジェクト間の距離を表明するグラフを導入する。

(make-graph
  (list sun cat1 100)
  (list sun cat2 200)
  (list cat1 cat2 150))

'(オブジェクト オブジェクト 距離) でエッジを作っていって、逆引きできるデータ構造を組み立てておく。

2点の絶対座標の差から距離を出すのはコストがかかる上に、絶対座標を持つモノのネットワークは回転について余分な情報を持っている。グラフを使った地図表現に変更することで、計算コストと、不要な情報を削減できた。

真空(オブジェクトがないところ)では場は値をもたないものとし、オブジェクトの周りの小さな領域だけ計算する。

オブジェクトとオブジェクトのコミュニケーションのために、巨大なグリッド(マトリクス)を計算するのは効果対計算コストが高すぎる。物理シミュレーションをしているわけではないので、伝播方法の詳細は無視する。後述の参考実装では、field−value(場の値)として各オブジェクトにスロットを持たせ、物理が情報をそこに書き込むようになっている。

距離と伝播速度から遅れを計算し、正しい順序で情報を伝播させる。

;;例 - 光速の1/3の速度の犬の鳴き声:
`(sound "bow-wow" ,inverse-square ,(/ 1 3))
;;-> 1m以内にネズミ、3m以内にネコがいた場合
`((,cat  :sound "bow-wow" 9)
  (,mice :sound "bow-wow" 3))
;;猫には9フレーム後、ネズミには3フレーム後に鳴き声が届く.

オブジェクトが場を振動させると、前述のグラフから近くのオブジェクトをそれぞれ取り出し、しかるべき時間の遅れののちにターゲットオブジェクトが監視しているローカルな場に、波の減衰曲線を距離に適用して得た値を書き込む。

[場の振動命令]は、減衰曲線(典型的な例は距離の逆2乗)と、光速(=1)に対する比率の形の伝播速度を付与されている。

参考実装

静止系のVOCネットワークを実装してみた。プロトタイピングでリストを乱用していて読みづらいのが恐縮ですが、コンセプトは大体表現できたと思う。

場を使った相対論的オブジェクト間コミュニケーション · GitHub

前回の記事と同じく、一つの太陽と2匹のネコの静止系で実験してみる。

f:id:ympbyc:20151113211141p:plain

(define (main . args)
  (let ([cat1 (make <cat> :fields '(sound temperature)
                    :body-temperature 34)]
        [cat2 (make <cat> :fields '(sound temperature)
                    :body-temperature 34)]
        [sun  (make <sun> :fields '(temperature electromagnetic)
                    :temperature 1000000)])
    (run-space (list cat1 cat2 sun)
               (make-graph
                (list sun cat1 100)
                (list sun cat2 200)
                (list cat1 cat2 150))
               800)))

結果:

f:id:ympbyc:20151114134200p:plain

時刻100: まず、太陽からの電磁ノイズがピンクの猫に到達する。ここでは光速を1として計算しているため、距離がすなわち時間の遅れとなる。同時刻に1000000℃あった太陽熱が、距離の逆2乗(100℃)まで下がってピンクの猫に到達し、猫は "too hot meow!" と鳴く。

時刻200: 100フレーム後には、太陽からの電磁ノイズと熱が緑の猫に伝わる。熱は減衰して25℃になっていて、気持ちよいポカポカ陽気なので、猫は "nice and warm meow!" と鳴く。

時刻400: 時刻100に発信され、光速の1/3で伝わる音波"too hot meow!"は、300フレーム後に太陽に到達し、太陽の周りの音場を揺らす。ここでは音については厳密に減衰を考慮していない。

時刻550: ピンクの猫が発した鳴き声が緑の猫の耳に届く。

時刻650: 緑の猫の鳴き声がピンクの猫の耳に届く。

まとめ

場を使ったオブジェクト間コミュニケーションというややこしそうな仕組みの割に、簡単に宣言的にオブジェクトを記述できるフレームワークを作れそうだ。

VOCの拡張 - 場の導入

昨日の記事でちらっと出てきた、VOCについて。1年半前にこんな記事を書いた。

ympbyc.hatenablog.com

VOC(Vision Oriented Communication)を初めて紹介した記事だが、それ以来VOCについてたまに考えていて、よりわかりやすい説明を思いついたことと、IoTへの応用を見据えた再定義を行いたいので、この記事をもってVOCの定義を改訂する。

メッセージは波であり、宛先は持たない

f:id:ympbyc:20151111224650p:plain

電話やメールと言ったテクノロジーの助けがないプリミティブな世界では、真に1対1のコミュニケーションは(接触以外に)あり得ない。

  • 眼はおおまかに400nm~800nmの波長の電磁波を受信して検波する。電磁波は波であり、ある程度の指向性こそ持たせられても特定の宛先を持つものではない。
  • 耳も音波について同様である。
  • 口はAM+FM変調した音波を送信する。
  • 身振りは可視光線を反射するパターンを変えることで、情報を光にエンコードする。

前にも言った喩えだと、信号が車のbrakeメソッドを呼ぶわけじゃなくて、ドライバーが信号を見てブレーキを踏むか否か判断する。

宛先を持ったメッセージが電話なら、宛先を持たないメッセージはアマチュア無線だ。

空間は場だ

波は場を伝わる。場は全ての地点になんらかの値を割り振られたグリッドと考える。場は、音場、電磁場等自然由来の場を使ってもいいし、シミュレーションでもいい。例えば文字列が値となる場があったって良いし、音場に方向を追加してベクトルにしたっていい。場をコミュニケーションの媒体として使う。シミュレートする方法は問わない。

受信 -> 計算 -> ブロードキャスト -> 再帰

1年半前の記事ではVOCのコンセプトと、その実装の一例を一つの記事にまとめてしまっていた。ここでは改めてコンセプトだけを説明する。

AI

VOCを実装するモノは、REPLの亜種REBLを実装する。REPLはRead Eval Print Loopで、通常Rは標準入力、Pは標準出力や標準エラーと、限定されたストリームに対する読み書きを指すが、VOCではReadとPrintを拡張し、多対多のコミュニケーションのため、それぞれReceiveとBroadcastとする。

Receive Eval Broadcast LoopでREBLだ。

Receive (受信)

自分が持つあらゆるセンサーそれぞれについて値を解析する。

シミュレーションの場合は自分の周りの場の値を読む。

Eval (評価)

受信した情報と、自分の状態から、自分の次の状態を計算する。

Broadcast (ブロードキャスト)

Evalされた自分の次の状態に応じて、影響を与える場を振動させる。「場を振動させる」っていってもそれほど難しく考えないこと。ようするに近くの誰にでも受け取れるメッセージを送り出すってことだ。本文に宛名は入ってもいいが、他のモノが見るのを妨げることはない。

Loop (再帰)

また受信待機に戻る。

アクターとの違い

VOCは実際Erlangのアクターによく似ている。違いはメールボックスの代わりに場を読み、メッセージを特定のプロセスに送る代わりに特定の場を振動させるという2点だ。

場のシミュレーションについて

実際のところ、場のシミュレーションは愚直にやると無駄が多いと思うので、実装方法は処理系に一任する。モノがある場所の値だけ計算すればいい。時間(光速度や音速)についてもシミュレートするのが理想だがなくて困らないならいらない。

Pseudo Code

改訂版VOCの実装はまだないので、ナイーブな疑似コード。

100℃の太陽と猫2匹の世界

;;気温がちょうど良いとニャーと鳴く(音場を揺らす)猫
(defn cat-with-no-mass [fields cat]
  (let [temp (read-field :temperature (:address @cat) fields)]
    (vibrate-field fields :sound (:address @cat) inverse-square
      (cond
        (> temp 30)
        "too hot meow!"
        (< temp 15)
        "too cold meow!"
        :else "warm and nice meow")))

;;温度場を照らす太陽
(defn sun [fields sun]
  (vibrate-field fields 
    :temperature 
    (:address @sun) 
    inverse-square 
    (:intensity @sun)))

;;減衰曲線を使って場を計算する
(defn vibrate-field [fields layer address decay val]
  ;; ...omitted...
  fields)

;;メインループ  -- とりあえず相対論は無視して絶対時間
(defn run-space [things]
  (loop [fields]
    (recur (merge-fields (pmap #((:ai %) fields (:property %)) things))))

;;初期設定
(run-space
 #{{:ai cat-with-no-mass
    :property (atom {:address [0 0 0]})
   {:ai cat-with-no-mass
    :property (atom {:address [100 100 100]})
   {:ai sun
    :property (atom {:address [10 10 10] :intensity 100})}}}
 #{:temperature [[[...]]]
   :sound       [[[...]]]})

シミュレーションと、IoTデバイスのネットワーク両方に同じコードが使えるとアツい。

本当はアドレスと、グローバルな場をなくしたい。場のバケツリレー(相互作用するローカルな場)とか、いくつか案はある。

とりあえず以上です。

IoTの意味がわかった

f:id:ympbyc:20151016161744j:plain

注) IoTの定義は知らないので、この記事で言うIoTは、「僕が考えるIoT」です。

最近まで気にしてこなかったけど、電子工作を始めてから興味が湧いてきたIoTについて。

現状はビーコンとかセンサーとかRFIDとかの弱いコンピュータ群とインターネットが作るネットワークを指してIoTと言っているように見える。

これでも十分面白いんだけど、今は過渡期の初期段階で、実はもっと面白いことが起ころうとしてるはずだ。

僕らにはどうしてもコンピュータは高価なものっていう固定観念があって、やっとビーコンやArduinoを惜しげも無くばらまけるようになってきたかどうかといったところだ。

本当は全てのノードが強いコンピュータになって初めて面白くなってくる。IoTは、「安いマイコンをばら撒こう」ってことじゃなくて、「汎用チップやモジュールがどんどん安くなるよ」って話なんだ。

強いコンピュータ、弱いコンピュータと言っているのはLinux搭載か否かとかではなくて、おおまかに

の全てを備えたものを強い、それ以外を弱いと表現している。BLEでデータを一方的に送信するビーコンやセンサは弱いし、wifiシールド等を付ければArduinoも強くなり得るということ。

強いコンピュータのネットワークは、OOP(メッセージングのOOP)やアクターモデルVOCの計算基盤になる。

つまり、大雑把に言うと実世界がコンピュータになるということ。

以前僕は、「OOPでは机に高さを訪ねると答えが返ってくるが、現実では僕らが自分で測らないといけない。OOPは実世界のシミュレーションには適していない。」という旨の主張をしていたが、どうやらOOPが現実に即するのではなく、現実がOOPに即するようになるようだ。じきに僕らは机と会話するようになる。

インターネットにつながる一つのデバイスを作ってIoTとか言ってちゃだめで、大量のボードを惜しげも無く使ってネットワークを構築して、ネットワーク単位でプログラミングするのがIoTなんじゃないだろうか。

というわけで、これからは

なんかが流行ると思います。みなさんも研究してみてはいかがでしょうか。

それから、実世界ではアドレスを持ったメッセージというのは稀なので、OOPアクターモデルよりも、VOCに近いコミュニケーションプロトコルを採用する方が賢いと予想します。ようするに1対1のメッセージではなく、多対多の 受信->計算->ブロードキャスティング のループを各ノードが実装するってことです。

VOCについてはこちらを参照してください。

ympbyc.hatenablog.com

カッコの少ないSchemeを作る

f:id:ympbyc:20150908160824p:plain

昨日の記事

ympbyc.hatenablog.com

の内容をScheme (gauche)で実験できるようにしてみます。

今回の記事は、上から順にGaucheのリスナに貼り付けていけば、動作が確認できるようにしてあります。

準備

まず、固定長引数とそれ以降の引数を取って、余った引数を結果にapplyする関数を作るマクロを作ります。define-syntax面倒臭いのでdefine-macroしちゃいます。

(define-macro defix
  (lambda (defclause . body)
    `(define (,@defclause . next)
       (if (pair? next) 
           (apply (begin ,@body) next)
           (begin ,@body)))))

(macroexpand '(defix (-car x xs) x))
;;=> (define (-car x xs . next) 
;;=>   (if (pair? next) 
;;=>     (apply (begin x) next)
;;=>     (begin x)))

(define-macro lamfix
  (lambda (params . body)
    `(lambda (,@params . next)
       (if (pair? next) 
           (apply (begin ,@body) next)
           (begin ,@body)))))

(macroexpand '(lamfix (x xs) xs))
;;=> (lambda (x xs . next) (if (pair? next) (apply (begin xs) next) (begin xs)))

ちょっと汚いけどあんまり気にしないで。

以降、すべての definedefix, lambdalamfix に置き換えて定義していきます。

リスト関数

(defix (cons x xs)
  (lamfix (f) (f x xs)))

(defix (car xs)
  (xs (lamfix (y ys) y)))

(defix (cdr xs)
  (xs (lamfix (y ys) ys)))

(defix (-car x xs) x)
(defix (-cdr x xs) xs)

(defix (<: x xs)
  (lamfix (y) (cons y (cons x xs))))

(defix (list x)
  (cons x '()))

(list 4 <: 3 <: 2 <: 1 -car)           ;;=> 1
(list 4 <: 3 <: 2 <: 1 -cdr -cdr -car) ;;=> 3

mapfilter の例も同様に、 defixlamfix をうまいこと使って作っていきます。

(defix (map xs f)
  (if (null? xs) '()
    (cons (f (car xs)) (map (cdr xs) f))))

(defix (filter xs f)
  (if (null? xs) '()
    (if (f (car xs))
      (cons (car xs) (filter (cdr xs) f))
      (filter (cdr xs) f))))

(defix (prefix f)
  (lamfix (x xs) (pa$ f (cons x xs))))

(define -map (prefix map))
(define -filter (prefix filter))

(list 5 <: 4 <: 3 <: 2 <: 1 
 -filter odd? 
 -map    (pa$ * 2) 
 -map    print)
;;=> 2
;;=> 6
;;=> 10
;;=> #<closure (cons cons)>

パイプライン演算子

-> も簡単です。

(defix (-> x)
  (lamfix (f)
    (lamfix (g)
      (f (g x)))))

(defix (<- x) x)

(-> 2
 -> (pa$ + 3)
 -> (pa$ * 4)
 <- number->string)   ;;=> "20"

できました!

混ぜて使う

固定長引数の関数は defix で定義して、可変長引数の関数は define で定義すれば、混ぜて使うこともできます。

(define (list . xs)
  (fold-right (lambda (x acc) (cons x acc)) '() xs)) ;;再定義したconsを使うため

(-> (list 1 2 3 4)
 -> (cut map <> (pa$ * 2))
 <- (cut map <> print))
;;=> 2 4 6 8 #<closure (cons cons)>

まとめ

Scheme処理系の上に、マクロを使って評価規則をちょっとだけ変えたLispを実装しました。Lisp式のマクロがあると言語の(見かけ上の)評価規則すら変えられるという例です。

リストの例(consの改造)は影響が大き過ぎるので実用は難しいと思いますが、固定長引数の関数を新しく作る時にとりあえず defix を使ってみるっていうのはやってみると意外と実用的かもしれません。


オススメ

Lispから可変長引数を引き算したらできること

f:id:ympbyc:20150907225639p:plain

Schemeから可変長引数を引き算したら -- Island Life

Shiroさんが面白い記事書かれてたので、前に実際に可変長引数をなくしたLispを作って発見したことを紹介します。*1

実装者とは全く別の、遊ぶ人の視点からの記事です。

この記事では意図的にマクロを一切使わないという制約のもと、いわばラムダ計算パズルをします。

左結合のカッコを省略する

少々恣意的ですが、

((compose f g) x)

;;こう書ける
(compose f g x)

こういうこと、引数の数が固定なのでカッコを省略しても一意に評価できます。関数を返す高階関数を使うときに便利。以下の例では全てこの仕組みを存分に活用します。

list関数もクオートも使わずにリストを作る

Shiroさんの記事で言及されていたように、可変長引数がないと (list 1 2 3 4) ができなくて不便です。これも一工夫でうまいことできます。要するに1つずつ cons していけばいいって話ですが、それでは面倒なので前項の仕組みを活用します。

まず cons, car, cdrの仕組みを思い出してみます。

(define (cons x xs)
  (lambda (f) (f x xs)))

(define (car xs)
  (xs (lambda (y ys) y)))

(define (cdr xs)
  (xs (lambda (y ys) ys)))

こうですね。*2 2値にconsを適用するとクロージャに評価されます。
ここでちょっと寄り道して carcdr を少しシンプルにした -car-cdr を作ります。

(define (-car x xs) x)

(define (-cdr x xs) xs)

;;すると

(cons 1 (cons 2 '()) -cdr -car) ;;=> 2

こうやって後置のリスト関数が作れるわけですね。同じ理屈で中置バージョンのcons、 <: を作ってみます。

(define (<: x xs)
  (lambda (y) (cons y (cons x xs))))

時間がかかりましたがこれを左結合カッコの省略と組み合わせると、やりたかったことが実現できます。

(cons 1 '() <: 2 <: 3 <: 4 <: 5) ;;=> '(5 4 3 2 1)

;;おまけ

(define (list x)
  (cons x '()))

(list 5 <: 4 <: 3 <: 2 <: 1) ;;=> '(1 2 3 4 5)

まあ普通の list よりはちょっと不便ですが見た目は綺麗でしょ?

リスト関数の後置、中置化

同じように他のリスト関数も後置または中置バージョンを作ると便利です。予め引数の順番を変更したリスト関数を作っておきます。

(define (map* xs f) (map f xs))
(define (filter* xs f) (filter f xs))
;;etc

(define (prefix f)
  (lambda (x xs) (pa$ f (cons x xs))))

(define -map (prefix map*))
(define -filter (prefix filter*))
(define -take (prefix take))
;;etc

こうすると

(list 6 <: 5 <: 4 <: 3 <: 2 <: 1
 -filter even?
 -map    (pa$ * 2)
 -take   2
 -fold   + 0)      ;;=> 20

便利でしょ?

パイプライン演算子

この仕組みで中置化できるのはなにもリスト関数だけではありません。関数合成 compose を中置化してF#のパイプライン演算子 |> や、clojureのスレッドマクロ -> のような関数を作ります。例によってマクロではなくただの関数です。

(define (-> x)
  (lambda (f)
    (lambda (g)
      (f (g x)))))

(define <- identity)

(-> 2
 -> (pa$ + 3)
 -> (pa$ * 4)
 <- number->string)   ;;=> "20"

中置とは言っても最初の値をラップする仕組みのためにちょっと不思議な見た目になります。 -> で計算をつないでいって、 <- で結果を取り出します。

まとめ

  • 可変長引数を撤廃することで、左結合のカッコを省略できます。
  • するといろいろ面白いことができます。

Land of Lisp最高です。

続き: Gaucheで実験できるようにしてみました。

ympbyc.hatenablog.com

ちなみに、もうずっと触ってなくて動くかわからないんですが、実験に使ったオレオレLispはこれです。

github.com

*1:この処理系ではデフォルトのカリー化もやっていて、これを使うともっと面白いこともできるんですが今回は割愛します。

*2:実際のScheme処理系ではconsセルをクロージャで表現するということはしないと思いますがここでは忘れます。