Hysteresis --- 値の歴史を遺す

 この記事はNextremer Advent Calendar 2019の25日目の記事です。ハッピーニューイヤー!(開き直り)

Hysteresis

 2019年の暮れに、ひとつCommon Lispのライブラリをつくりました。これは、このライブラリのもつ値設定マクロsetfを通してシンボルに値を設定するとそのシンボルの値の履歴が残るようになる、というものです。

github.com

動機

 ゲーム(cl-sdl2)とかジェネラティブアート(sketch)とかをCommon Lispでやっていると、Common Lisp単体では画面への描画ができないためCFFIを通して描画スレッドにコールバック関数を渡してやることがよくあります。REPLからコールバック関数を変更できたりして便利なのですが、lambda式なんかを渡していると、エラー時に差し替えようがなくてけっきょくCL処理系のスレッドごと落としてロードしなおすことになります。あまりREPL駆動のよさみを受けていない。

 この問題の解決方法のひとつとして、名前 (Common Lispのbindings)を通じて関数を渡すようにすればいいなあーと2019年12月ごろにおもいつきました。そのついでに、エラーが起こった瞬間に値を巻き戻す的な操作があると便利かもしれない、と思ったため実装しました。

 想定利用ケースとして以下のようなものを想定しています:

  • おもむろにシンボルの束縛していた過去の値にもどしたい
  • エラー時、自動で1個前の値にもどしたい

概要

 Hysteresisではシンボルに履歴というものを導入します。この履歴つきシンボルをhistorized symbolsとこのライブラリでは呼ぶことにしました(historizedって「歴史に名を残す」という意味らしいですがライブラリの内容的に洒落っぽくていいなと思いました)。まあ履歴つきシンボルとかおおげさな胡椒ですが、専用の構造体にリングバッファがついていて、リングバッファの最大長以内であれば値が保持されるシンボルがある、というていどのものです。

使いかたの基本

値の設定と取得はhysteresis:set-valuehysteresis:get-valueを用います。シンボルをキーとして内部のハッシュテーブルに保持された値履歴に書き込んだり取得したりします。

CL-USER> (hysteresis:set-value 'foo 42)
42
#(HYSTERESIS:HISTORIZED-SYMBOL ...)
CL-USER> (hysteresis:get-value 'foo)
42

でもこれは内部API的であまりユーザに利用されることはあまり想定していません。hysteresis:hsetqというsetqもどきを通じて、シンボルマクロを定義するのが便利です。利用感はこんなかんじです。

CL-USER> (hysteresis:hsetq hoge 42)
42
CL-USER> (setf hoge 42)
42
CL-USER> hoge
42

歴史を戻したり現在に戻ったりする

 履歴つきシンボルは、もちろん過去の値に遡ってシンボルの値を戻すことができます。まずは値を設定しておきます。

;; 履歴に値を設定する (きっとミュージックプレイヤーの現在の曲とかで利用している?)
CL-USER> (hysteresis:setq song "Walking In The Sun")
"Walking In The Sun"
CL-USER> (hysteresis:setq song "My Hero")
"My Hero"
CL-USER> (hysteresis:setq song "Half The Man")
"Half The Man"

このとき、現在の値は"Half The Man"です。これを唐突に2つ前の値"Walking In The Sun"に戻したいと思いました。こんなときhysteresis:revertを用いると、全ての履歴つきシンボルの値を2つもどすことができます。

CL-USER> song
"Half The Man"
CL-USER> (hysteresis:revert 2)
NIL
CL-USER> song
"Walking In The Sun"

わーお。でも、やっぱり最新の値に全てもどしたい場合もありますよね(たとえば過去の値もヤバかった的な)。そんなときはhystresis:presentを用いると、最新の(現在の)値に戻ります。

CL-USER> (hysteresis:present)
NIL
CL-USER> @song
"Half The Man"

値ったら最強ね(9)。

履歴つき関数

さて、冒頭ではゲームとかでコールバック関数がうんぬんという話がでてきました。もちろんこのライブラリでは関数も履歴つきにすることができます。hysteresis:hdefunの登場です。

CL-USER> (hysteresis:hdefun foo (x)
           x)
#<COMPILED-LEXICAL-CLOSURE #x302000DB1C8F>
CL-USER> (foo 42)
42
CL-USER> (foo 'hoge)
HOGE
CL-USER> (foo "wow")
"wow"

そしてもちろん履歴を辿りたい場合がある(はず)です。そんなときにはhysteresis:revertを用いることができます。

CL-USER> (hysteresis:hdefun foo (x)
           (list x x))
#<COMPILED-LEXICAL-CLOSURE #x302000E7309F>
CL-USER> (hysteresis:hdefun foo (x)
           (format nil "+~a+" x))
#<COMPILED-LEXICAL-CLOSURE #x302000ED2D6F>
CL-USER> (foo 'hoge)
"+HOGE+"
CL-USER> (hysteresis:revert 1)
NIL
CL-USER> (foo 'hoge)
(HOGE HOGE)

 こんな感じで、履歴を遡ったり現在側にもどってきたりできます。機能としてはとても単純なので、このような感じです。

 改良できそうな部分は多々あれど、これは使えるライブラリなのかしら…?? …うーん、なぞ。

今後の展望

  • hysteresis:hunintern
    • 通常のcl:uninternすると、内部ハッシュテーブルから値が消えないため
  • テストを書きたい
    • リングバッファが手書きなので、まじでみっちりと!!
  • REPL操作を意識した情報取得や一覧表示?
    • 履歴の内容確認
    • 履歴つきシンボル一覧
  • 実用する(ゲームとかで?)

まとめ

シンボルに履歴を持たせるライブラリをつくりました。しかし、このアイデアに需要があるのか、ほんとうに問題(動機)の解になっているのか、などが謎なのでこれから実用してみて判断を下す段階のものだと思います。