認証付きプロキシ環境でも(多少は)快適なpackage-installの利用
Emacs24を入れてから、list-packages
で適当にパッケージを追加してきたのですが、そろそろ.emacs.d/init.el
のポータビリティが怪しくなってきたので、環境を見直し始めました。当初は、CaskやEl-getといったモダンな?パッケージマネージャを使おうと意気込んでいたのですが、認証付きプロキシ環境でどうもうまく動きませんでした…。プロキシが原因なのかはよく分かりませんが。
今後も振り回されるかも知れないと思うと、解決は目指さずに(手元の環境下で)実績のあるデフォルトのpackage-install
に頼る方向にしました。
結果、「結局 package.el に戻ってきた / マスタカの ChangeLog メモ」を参考に以下のようにしました。package-refresh-contents
の呼び出しを、新しいパッケージのインストール時に限定している辺りが唯一のポイントです。呼び出すとプロキシのユーザ名・パスワードを求められるため、むき出しにするとEmacsを起動するたびに死にたくなります。かといって呼ばないとインストールできないためこうなりました。
ユーザ・パスワードの入力が新しいパッケージをインストールするときに限定されるので、「多少は」マシかと思います。
(require 'package) (add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t) (package-initialize) (defun install-packages (packages) (let ((refreshed nil)) (dolist (pack packages) (unless (package-installed-p pack) (unless refreshed (package-refresh-contents) (setq refreshed t)) (package-install pack))))) (install-packages '(auto-complete magit markdown-mode))
まあsocatなどでローカルにプロキシサーバを立てて認証を代理させるのが筋な気はしますが…。書いてしまったものは仕方がない!
ちなみに、bashの環境変数に設定しているhttp_proxy
やhttps_proxy
のプロキシのアドレスは認識してくれたのですが、一緒に書いた認証情報は無視されるようでした。
2015年に作っていたもの
2015年に作ったものまとめ。1つ例外を除いて全部Common Lispでした。
全体的に飽き性っぷりが垣間見えるだけな気もしますが、来年どれかは発展させていきたいです。
オセロ
(旧版:sample-of-eshamster/othello-cl · GitHub)
作るものに困ったらとりあえずオセロを作れば良いんじゃないか、と思って作りました。学習機構は作ってないですが、簡単な静的評価関数+αβ探索とUCT(Upper Confidence bounds applied to Trees)ぐらいは作りました。あとは簡単なCUIインタフェースつきです。
適当に作った結果、ゲーム開始地点から終局までのランダムシミュレーションが(初期局面へのリバース込みで)秒間30回というひどい値になっていたので、しばらく最適化して秒間200回ぐらいにしました。さらに、この年末にも最適化して遊んでいたところ、秒間1000回ぐらいになりました。相場が分かりませんが、実用にはまだ桁が足りない気がします*1。
備忘録も兼ねて最適化周りの話はそのうち記事にしておきたいです。
ちなみに、旧版は右も左も分からない頃にClozure CL上で書いていたもので、新版はSBCLにも対応しつつパッケージ周りを少し整えて移植したものです。
cl-lazy
Land of Lispにはマクロによる遅延評価の実装が出てきます。面白そうだったので自分でも書いて色々遊んでみたのがこのライブラリです。
遅延評価と言えば無限長数列というイメージがあったので、とりあえずフィボナッチ数列を作ってみました。以外と手間取ったので、まずはマクロで苦労した部分を覆い隠してみました。それでも、数学的な記法による定義(漸化式)と比べると分かりにくいのでリードマクロで数列定義文法を作ってみました。
そんな感じで、思いの他Common Lispのマクロ方面を色々触ることができて楽しかったです。
結果ですが、#<fib[n] = 0, 1, (+ fib[n-2] fib[n-1])>
で無限長フィボナッチ数列が定義できるようになりました。ワンライナーで数列を定義できる言語は数あれど、ここまで分かりやすいものはないんじゃないか、と密かに思ってます。
遅延評価部分よりも数列処理部分を取り出して発展させる道はありかもと思いつつ、自分で利用する場面がないので放置してます。
cl-prime-number
cl-lazyの遊びの一貫です。無限長素数列を作って遊んでいます。具体的には、素因数分解や、分解後の素数指数表現の中で最小公倍数や最大公約数を求める関数を作っています。
ちなみに、Gaucheの持つ素数ライブラリは恐ろしく速いですね。例に出ている素因数分解をcl-prime-numberに投げても、結果が返るよりもCtrl+Cを押す誘惑に負ける方が速いです。
MAL (Make a Lisp)
Lisp方言の一つであると同時に、Lispインタプリタを実装するためのチュートリアルでもあるMALに手を出してみました。この中では珍しく、Lispではなく前々から興味のあったF#で書いています(F#によるMAL実装は既にあったので、esh-fsharpというフォルダに入れています)。
ステップ4まではやりましたが、ステップ5の末尾再帰の実装がなんだか関数型っぽくなくなりそうで尻込みしている*2のと、同時期にLispが楽しくなってきたので頓挫しています。
cl-naive-bayes
単純ベイズ分類器(もしくはナイーブベイズ分類器)です。普段使っているInoReaderに若干不満があった*3ので、RSSリーダでも自作してみようかと思い、その部品として作りました。精度は問わないので、とにかく簡単に使えるものとして作っています。
単純ベイズ分類器としては一通りできあがっています*4。が、新しいおもちゃ下記のps-experiment関係の方が面白くなってきたのでRSSリーダ自体や、それに向けた分類器への機能追加は止まっています。そのうち再開する気でいますん。
kaggle-titanic
現実の統計的課題を世界中のデータサイエンティストが競って解くというKaggleなるものがあります。某氏にそそのかされて、そのチュートリアルとして有名(らしい)なタイタニックの生存者を推定する問題をCommon Lispで解いてみました。
まずは上記のベイズ分類器を試してみて、次はもっとマシな分類器で…というつもりでいました。が、CSVデータ処理マクロを書いたところで満足してしまったので、最初の段階で止まっています*5。テストデータの正解率は77%ぐらいで箸にも棒にもかからない感じです。
マクロの成長過程の一例としては見れそうなので、そのうちたぶん記事にします。
caveman-sample
深町さん作のWebフレームワークCaveman2を使ったサンプルです。HTML直書きやデフォルトのDjulaの代わりにcl-markup、JavaScript直書きの代わりにParenscript、とCommon Lispづくしで書いて見ています。
今のところ、Angular.jsでアコーディオンパネルを作ってみたり(上記のRSSリーダに向けた実験)、WebGL(Three.js)を動かしてみたりして遊んでいます。
ps-experiment
上記のcaveman-sampleを作る中でParenscriptの不満なところに色々マクロを被せていたのですが、分量が増えてきたので独立させました。
ここ数ヶ月は大体これをいじっていた気がします。基本的にはJavaScriptでしかないところはよりJavaScriptらしく、逆にCommon Lispとして書ける部分はよりCommon Lispらしく、という方向で進めています。
cl-ps-ecs
ゲームプログラムのアーキテクチャの一つにEntity_component_system(ECS)というものがあります。「[GDC 2015]エンジンとツールがないなら自作しよう。「World of Tanks Blitz」ローンチまでの道のりを開発者が振り返る - 4Gamer.net]」で知って以来、自分で書いてみたら面白そうと思っていたものです。以前はXNAでオブジェクト指向な簡単なライブラリを書いたりはしてましたが、今さらXNAはないよなあ…と移行先に迷って放置していました。
今やCommon Lispという新天地を手に入れたので、せっかくだからWeb GL(→JavaScript→Parenscript→ps-experiment)でも組み合わせて作ってみようか、と動き出したところです*6。
進捗としては、ようやくcaveman-sampleの方でお試し第一段を書けるようになったところです。が、まだ使える代物には全然なっていません。
特徴として、(今の所)Common LispとParenscript(+ ps-experiment)の共用コードとなっています*7。これについては、実用的な価値を求めたというより、ps-experimentの皮を被せることでどこまでCommon Lispっぽく書けるかを試している感じです。
ちなみにps-experimentの安定化にもかなり貢献しています。なにしろ数歩ごとにps-experimentのバグを2つ3つ踏み抜いていたので…*8。
- 間接的に関連する記事:マクロ展開時に副作用を起こすことの恐ろしさ - eshamster’s diary
その他
ひっそりとScheme(Gauche)やClojureを触ったりもしてましたが、githubにはまだ痕跡がないです。
また、年始にUnityを少し触っていました。簡単なモデルを作って動かしたりユニティちゃんを動かしたりしてみましたが、特に形になったものはないです。プロジェクトがすぐアセットだらけになりますが、こういうMByte級のファイルがGitの履歴に溜っていくのは気持ち悪いですし、かといってバージョン管理なしでは怖くて何も作れない体質になってしまったので、そこで挫折しています。RubyのGemfileのようなものってないんでしたっけ。アセット編集した場合どうするんだとかもあるので、簡単でないのは分かるのですが。一応自作or編集したスクリプトだけ取り出した残骸はありますが、これだけでどうなるものでもないです…。
来年に向けて
Lispという新天地を手に入れたおかげでプログラミングのモチベーションがかなり回復したので、引き続きこの方面を掘っていきたいと思います。Lispは何もかも全てLispで書きたくなる魔力に満ちていますが、他の言語にもまた足を伸ばしてみるつもりです。候補は色々浮かびますが、まあ気の向いたところに進むつもりです。
他にGithub, CircleCI, (ぼっち)Slackと環境の近代化も進めてきたので、引き続き色々なツールを漁っていけたらと思います。
*1:手生成が明らかなボトルネックになっているので、ビットボードに手を出す必要がありそうです
*2:解説を読んだ限りは、ステップ4から差分それだけでいけるのかー、と感心はしたのですが
*3:とはいえ、今は亡きGoogle Readerからの乗り換え先として検討した中では圧倒的に良いものでした
*4:はじめての怪しげな英語ドキュメントを書いてみたりもしました
*5:一応、雑なアンサンブル単純ベイズは書いてみましたが、特に精度は上がらなかった…という所で飽きました
*6:ちなみに、Parenscriptにこだわっているのは、Web GLヘビーに作るとただのJavaScriptになってしまって悔しいので、Common Lispで書ける範囲を確保したいといった辺りが理由です
*7:ps-experimentで"xxx.ps+"系のマクロをいくつか提供しています。例えば"defun.ps+"ではCommon Lispの関数とJavaScriptの関数(を生成するCommon Lisp関数)が生成されます。現状これを利用して全て書いています。肝心のパッケージ周りは作り込めていないので、use(use-package)でお茶を濁していますが…。
*8:テストはきちんと書いているんですけどね…。パッケージや評価順序周りのやっかいなバグが中々つかまらないです
2015年の斑鳩
せっかくブログを作ったので斑鳩についても2015年のまとめ。
目標と進捗
特にデスクトップPCがお眠りになって以来、Lispと音ゲーにかまけてほぼサボっていました…*1。
3400万という次なる理想に対する以下の小目標(後付け)はどれも道半ばといったところでしょうか。
- 解放合戦の残1秒パターンを通しに組み込む
- 元は残7秒パターン
- 全体的な精度の向上
- C4外周パターン(途中まで)を通しに組み込む
- 元は内周全滅パターン
解放合戦
点数目的というよりは、更新かかってる時に残1秒を狙えるぐらいの胆力がないと更新なんかできまい、という試練としての意味合いが強いです。また、通しに慣れることの一貫でもあります。現状はまだ成功率が低いです。個々のパーツはできてきてますが、練習量が絶対的に足りていません…。
参考に残7秒パターン解説。下記で残7秒になります。一回死んでも撃破が間に合うので、時間切れでボスボーナス0点の悲しさに耐えられない人にお勧めです。扇状弾の開始直後は弾がばらけていて個人的に苦手なので、いったん撃ちやめて避けに徹しています*2。60回のカウントは慣れると半自動でできるようになります。ちなみに、+5回吸うと残2秒ぐらいになります。
- ショットは撃たない
- 扇状弾が来るまでは解放を撃ち返す
- 扇状弾が来たら60回は吸収に徹する
- あとは撃破まで解放を撃ち返す(10回ほど)
精度向上
3400万に対して余裕のないパターンで(当面)挑むつもりなので、精度向上が重要になっています(3300万までは目標+100万ぐらいのパターンを使って多少のミスは許容という感じでしたが)。今までに比べ、反復練習で手に覚えさせる部分よりも、頭で考えてなんとかする部分を意識しました。再現性のあるバグは必ず直せるはずだ、という信念に基づいて頭を使ってました*3。おかげで2度同じミスをすると、「これは直せる!」と嬉しく思えるぐらいには脳回路ができあがってきました。
成果としては、全体にちょっと間を空けたぐらいならほぼ精度は落ちないぐらいの自信はついてきました。といっても、安定性の下限と上限を引き上げた感じなので、上限まで引っぱりあげるには密な反復練習しかないです。
個別に見るとC3は目に見えて効果が出ていて、今が一番安定しているような気がします。そのC3を含めても全体的にまだまだ不安ポイントは多いので、段々と潰していきたいです。
C4外周
3300万とれたらやろうとしていたC4外周(の一部)。内周全滅+30万ぐらいになりました。通しに組み込むには現実まだ厳しいかなというところで投入してみて、今は偶にうまくいく程度に安定してきました。まだ全体的な不安定さは残りますが、大体2, 3箇所ぐらいまで不安ポイントを絞れてきた感じです。
始めてしばらくは、これ自分の力量では安定する気がしないのだが…と思っていましたが、意外と芽は見えてくるものですね。
来年
大目的の3400万は変わっていないので引き続き前進です。
何よりも、早いところデスクトップPCを輪廻転生させて、家斑鳩環境を復活させないといけないですね…。
おまけ
斑鳩納めの結果
12月30日にHeyにて斑鳩納めをしてきました。3回プレイして納めは残6の3180万でした。更新いけるかもというペースで解放合戦に突入し、扇状弾に突っ込んで死にました。ついでにもう1回死にました。突入時に迷わず残1秒パターンを狙いにいけたというのはまあ収穫です。失敗してたらしょうがない訳ですが…。
投稿した動画
そういえば、こんなネタを投稿してました。見直してみるとテロップ直前は中々面白いパターンになってますね。あと、いつのまにか「C3の人」タグが付いてました w
マクロ展開時に副作用を起こすことの恐ろしさ
Lisp Advent Calendar 2015の23日目の記事です。
すごく及び腰でしたが、ずっと空いていたので、えいやで登録してみました。
マクロ展開時に副作用を起こすな危険、という内容です。
前書き
On Lisp: マクロのその他の落し穴によると、「Lispは,マクロ展開を生成するコードは 第3章で論じた意味で純粋に関数的であるものと予期している. 展開を行うコードは引数として渡された式にのみ依存すべきで, 値を返す他には周囲の世界に影響しようとすべきではない.」とあります。下線を言い換えると副作用を起こすなということになると思います。NG例の一つに、マクロの展開回数を数えようとしてグローバル変数 *nil!s*
に触る以下の例が示されています。
(defmacro nil! (x) ; 誤り (incf *nil!s*) `(setf ,x nil))
正直に言うとその下にある説明では結局いつ困るのかピンと来ませんでした。が、Parenscriptをいじっていてこれで散々ハマった*1ので、勉強結果を展開してみます。次を伝えることが目標です。
- どう恐ろしいのかという感覚
- どうしてそうなるのかという理屈
大多数のLisperにとっては分かりきった話だろうと思いつつ、次のような感じで進めていきます。
- 前座:Parenscriptの簡単な紹介
- 怖さが伝わるかもしれない例
- 解説
- 実際にハマった話
- まとめ
前座:Parenscriptの簡単な紹介
ParenscriptはCommon Lispの(サブセット)コードをJavaScriptコードに変換してくれるライブラリです。下のように ps:ps
マクロの中にCommon Lispコードを書くとJavascriptコードを文字列として出力してくれます。
CL-USER> (ql:quickload :parenscript :silent t) (:PARENSCRIPT) CL-USER> (ps:ps (test-func 10 20)) "testFunc(10, 20);" CL-USER> (ps:ps (funcall (lambda (a b) (+ a b)) 10 20)) "(function (a, b) { return a + b; })(10, 20);"
Lispとしては外せないマクロもサポートされていて、大きくは次の2つの方法で定義できます。
;; ps環境内でdefmacroを呼ぶ方法 CL-USER> (ps:ps (defmacro test-macro (a b) `(+ ,a ,b)) (test-macro 10 20)) "10 + 20;" CL-USER> (ps:ps (test-macro 20 30)) ; グローバルに定義される "20 + 30;" ;; defpsmacro による方法 CL-USER> (ps:defpsmacro test-psmacro (&rest rest) `(* ,@rest)) TEST-PSMACRO CL-USER> (ps:ps (test-psmacro 10 20 30)) "10 * 20 * 30;"
ps環境内でのdefmacro
は内部的にはdefpsmacro
を呼んでいます。このため、どちらも同じように使えます…だったら良かったのですが…。
何が起きるのか
Parenscript用のマクロ(以下、PSマクロ)定義を2種類紹介しました。これらはPSマクロを管理するグローバルな変数parenscript::*macro-toplevel*
に登録されるタイミングが異なります。
- ps環境内での
defmacro
: 展開「時」にマクロ定義が登録される defpsmacro
: 展開「後」にマクロ定義が登録される
従って、ps環境内での defmacro
の方が展開時に副作用を起こすというまずい動作をしています。これが何を引き起こすのか見てみます。
準備
REPLや一つのスクリプトファイルで試していても中々起きない現象であるため、小さなプロジェクトを一つ起こして、quicklispの配下に置きます。
CL-USER> (ql:quickload :cl-project :silent t) (:CL-PROJECT) CL-USER> (cl-project:make-project (merge-pathnames #p"test-ps-eval-order" ql:*quicklisp-home*) :author "eshamster" :licence "MIT" :depends-on '(parenscript)) writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/.gitignore writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/README.markdown writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/README.org writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/test-ps-eval-order-test.asd writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/test-ps-eval-order.asd writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/src/test-ps-eval-order.lisp writing /home/esh/.roswell/impls/ALL/ALL/quicklisp/test-ps-eval-order/t/test-ps-eval-order.lisp T
次に、できあがったsrc/test-ps-eval-order.lisp を編集して次のように2種類の方法でParenscript用のマクロを定義してみます。また、これらのマクロの展開結果を確認するため、print-ps
関数を作成 & exportします。なお、eval-when
がないと(print (ps (test-defpsmacro)))
の部分でマクロが動きませんが、本題ではないので詳細略です*2。
さらに、できるだけクリーンな環境で実行したいので、Roswellスクリプトを一つ起こして、上記のprint-ps
関数を呼び出すコード(とParenscriptをロードするコード)を追加します。
# ※OSコンソール $ cd 任意の場所 $ ros init execute.ros
実行
さて実行です。
$ ./execute.ros To load "test-ps-eval-order": Load 1 ASDF system: test-ps-eval-order ; Loading "test-ps-eval-order" [package test-ps-eval-order] ----- From test-ps-eval-order::print-ps ----- "ok = 'expanded by test-defpsmacro';" "ok = 'expanded by test-defmacro-in-ps';"
どちらの定義もパッケージ内部ではうまく動いているようです(下3行)。次はパッケージ外(execute.ros側)から呼び出すため、以下を修正します。
- src/test-ps-eval-order.lispのexportに両マクロを追加
- execute.rosのmain関数にこれらを呼び出すコードを追加
;; src/test-ps-eval-order.lisp (defpackage test-ps-eval-order (:use :cl :parenscript) (:export :test-defpsmacro :test-defmacro-in-ps :print-ps))
;; execute.ros (defun main (&rest argv) (declare (ignorable argv)) (print-ps) (princ "----- From execute.ros ----") (print (ps (test-defpsmacro))) ; ここと (print (ps (test-defmacro-in-ps))) ; ここの2行はprint-ps関数と同じコード (fresh-line))
そして実行。
$ ./execute.ros To load "test-ps-eval-order": Load 1 ASDF system: test-ps-eval-order ; Loading "test-ps-eval-order" [package test-ps-eval-order] ----- From test-ps-eval-order::print-ps ----- "ok = 'expanded by test-defpsmacro';" "ok = 'expanded by test-defmacro-in-ps';" ----- From execute.ros ---- "ok = 'expanded by test-defpsmacro';" "ok = 'expanded by test-defmacro-in-ps';"
なんだ問題ないじゃないか…と思って、もう一度実行してみます。
$ ./execute.ros To load "test-ps-eval-order": Load 1 ASDF system: test-ps-eval-order ; Loading "test-ps-eval-order" ----- From test-ps-eval-order::print-ps ----- "ok = 'expanded by test-defpsmacro';" "ok = 'expanded by test-defmacro-in-ps';" ----- From execute.ros ---- "ok = 'expanded by test-defpsmacro';" "testDefmacroInPs();"
なんということでしょう。1回目と異なり、execute.ros側だけps環境内のdefmacro
で定義したtest-defmacro-in-ps
マクロが消えています(関数扱いされています)。
- 同じように書いたのに結果が違う…
- → 書いたコードをいくら眺めても原因が分からない
- 2度実行すると結果が変わる…
- → 再現条件に確信を持てないため、色々いじっても直ったのか判断できない
見た瞬間デバッグする気力が削られる要素に満ちています。
解説
どうしてこうなったかを解説し、さらに、現象を再現する小さなコードを書いて動作を眺めてみます。
どうしてこうなった
上記の2回連続実行の出力を良く見ると、本体であるprint-ps
の出力の手前に違いがあります。1回目は[package test-ps-eval-order]
の出力がありますが2回目はありません。quicklisp/setup.lispを見てみると、これはコンパイル時のみ出力されるメッセージのようです*3。
(defun macroexpand-progress-fun (old-hook &key (char #\.) (chars-per-line 50) (forms-per-char 250)) ;; ~略~ (show-package (name) ;; Only show package markers when compiling. Showing ;; them when loading shows a bunch of ASDF system ;; package noise. (when *compile-file-pathname* (finish-line) (show-string (format nil "[package ~(~A~)]" name))))
ここから、以下の違いにより1回目と2回目で結果が変わったと推測できます。マクロ展開時の副作用はバイナリには残らないことに注意します*4。
- 1回目:test-ps-eval-orderをコンパイル*5、続けてそれをロードしてexecute.rosを実行
- コンパイルから実行までが同じ環境で行われる
- → マクロ展開時の副作用(
test-defmacro-in-ps
マクロの定義)はバイナリには残らないが、環境には残っている - →
test-defmacro-in-ps
マクロの定義がexecute.rosからも見える
- 2回目:コンパイル済みのtest-ps-eval-orderをロードしてexecute.rosを実行
- コンパイルと実行が異なる環境で行われる
- → マクロ展開時の副作用はバイナリには残らないし、そのため環境にもロードされない
- →
test-defmacro-in-ps
マクロの定義がexecute.rosからは見えない
結局のところマクロ展開時の副作用は、ライブラリに変更がない場合はコンパイルを省略しても結果は変わらない、という(妥当な)仮定を崩すことになります。
なお、その他2点の疑問は以下のように説明できます。
- なぜ、test-ps-eval-orderライブラリ内部からは常に
test-defmacro-in-ps
が見えるのか- バイナリには
test-defmacro-in-ps
マクロが既に展開された状態で記録されているため- ps:psマクロによる
test-defmacro-in-ps
マクロの展開もコンパイル時に行われる
- ps:psマクロによる
- バイナリには
- なぜ、
test-defpsmacro
は常にどこからでも見えるのか- バイナリにParenscript用マクロ定義(=
parenscript::*macro-toplevel*
への登録)を行う処理自体が残るので、ロード時に定義が実行されるため
- バイナリにParenscript用マクロ定義(=
小さく再現してみる
解説のためというよりは、現象をより剥き出しにするための小さなコードを書いてみます。
2つのファイルを用意します。1つはライブラリのつもりでtest-lib.lispを、もう1つはこれを利用するアプリケーションのつもりでtest-app.rosスクリプトを用意します。
test-lib.lisp
(eval-when (:compile-toplevel :execute :load-toplevel) (defvar *hoge-func-list* nil)) (defmacro defhoge (name &body body) `(progn (pushnew ',name *hoge-func-list*) (defun ,name () ,@body))) (defmacro defhoge-wrong (name &body body) (pushnew name *hoge-func-list*) ; 誤り `(defun ,name () ,@body)) (defhoge lib 1) (defhoge-wrong lib-wrong 2) (defun print-all-hoge () (dolist (hoge (reverse *hoge-func-list*)) (format t "~A from ~A~%" (funcall hoge) hoge)))
外の環境に触れたくなるのは大抵define系マクロだろうと思い、test-lib.lispではdefhoge
というhogeを定義するマクロを提供します。defhoge-wrong
も同様ですが、マクロ展開時に登録を行うという間違った動作をします。また、それぞれを利用して2つのhoge、lib
とlib-wrong
を定義します。さらに、hogeを登録順に出力するprint-all-hoge
関数も提供します。
test-app.ros
#!/bin/sh #|-*- mode:lisp -*-|# #| exec ros -Q -- $0 "$@" |# (defvar *load-kind* 0) ; ここを書き換えて実行する: 0, 1, 2 (case *load-kind* (0 (load "test-lib.lisp")) (1 (compile-file "test-lib.lisp" :output-file "test-lib.fasl" :print nil :verbose nil) (load "test-lib.fasl")) (2 (load "test-lib.fasl")) (t (error "arg error"))) (defhoge app 10) (defhoge-wrong app-wrong 20) (defun main (&rest argv) (declare (ignorable argv)) (print-all-hoge) (fresh-line))
test-app.rosでは上記のtest-lib.lispをロードして2つのhoge、app
とapp-wrong
を定義し、print-all-hoge
を呼び出して登録済みhoge一覧を出力します。ロードは*load-kind*
の値に応じて3種類のいずれかの方法で行います。
*load-kind*
が0の場合:test-lib.lisp自体をロード
$ ./test-app.ros 1 from LIB 2 from LIB-WRONG 10 from APP 20 from APP-WRONG
*load-kind*
が1の場合:test-lib.lispをコンパイルし、続けてtest-lib.faslをロード
$ ./test-app.ros 2 from LIB-WRONG 1 from LIB 10 from APP 20 from APP-WRONG
*load-kind*
が2の場合:コンパイル済みtest-lib.faslをロード(※事前に1のケースを動かすこと)
$ ./test-app.ros 1 from LIB 10 from APP 20 from APP-WRONG
ここまでの説明で原理は分かるはずなので解説は省略します。念のため、最初のtest-ps-eval-orderの例では、1回目の実行は*load-kind*
が1の場合に、2回目の実行は*load-kind*
が2の場合に相当します。なお、この例では*load-kind*
を固定している限り、何度実行しても実行結果は変わりません。
余談ですが、hogeを記録する*hoge-func-list*
をdefvar
ではなくdefparameter
で定義すると、*load-kind*
が1のときの結果が2の場合と同じになります。定義済みの変数を上書きするdefparameter
と上書きしないdefvar
の挙動の違いですね。
実際にハマった例
最後にこの記事の発端となったコードを。Parenscriptをもう少し便利に使えないかと色々実験をしているps-experimentというライブラリを作っています*6。
この中で、ps環境下で利用できるdefstruct
のサブセットを作ったのですが、これを利用するコードでエラーが出て散々にハマりました。定義の一部を載せます。冒頭のパース系の関数の定義は本題と無関係なため省略します。
コメントにありますが、アクセサの定義で利用しているdefmacro
が問題です。ここで注意ですが、defpsmacro
で定義したParenscript用のマクロは、結局ps環境下で展開されます。このため、defpsmacro
下でのdefmacro
はps環境下でのdefmacro
と実質上同じものです。
アクセサがマクロ展開時に定義されてしまうため、このdefstruct
で定義した構造体を別のライブラリから使おうとすると、アクセサだけ見えない(ことがある)という問題に悩まされることになります(なりました)。
厄介なことに、この問題はps-experimentのテストでは検出されませんでした。テストでは同じ環境下でdefstruct
による構造体定義とそのテストコードをロードするため、問題なく「動いてしまいました」。上記のtest-libとtest-appの例で言うと、test-app側でのdefhoge-wrong
の利用に相当するケースです。
結局どうしたのかですが、ps環境下でのdefstruct
はデサポートすることにしました。代わりに、これをラップしてトップレベルで利用するために用意していたdefmacro.ps
マクロを直接提供することにしました。
defxxx.ps
系マクロはトップレベルでParenscript用の色々を定義するためにps-experimentで用意しているマクロ群です。全体として、defpsmacro
をdefmacro
で、その他defxxx
をdefxxx.ps
で置き換えた以外、見た目に大きな違いはありません。
ただ、マクロ展開時にグローバルな値を読み込んでいる箇所があり、問題がないか気にしています。具体的には、include(スロット定義の継承)を実現するために、parse-defstruct-name-and-options
が*ps-struct-slots*
(上記では省略)というグローバルなハッシュを読み込んでいます。このハッシュへのスロットの登録はregister-defstruct-slots
で行っています。eval-when
による指定で、下記のようなコードをコンパイルしたときにも、childのマクロ展開よりも早い段階でparentの登録を行う形になっています(かつマクロ展開時の副作用を避けています)。ここのハッシュ読み込みは明らかに「純粋に関数的」でないため怪しいのですが、当面これで様子を見ようと思っています。
(defstruct parent a b) (defstruct (child (:include parent)) c)
現状で見えている怪しげな動作というと、コンパイル→ロードとすると同じ定義が2度実行されるというものがあります。が、同じもので上書きするだけなので大抵問題ない…はず。ちなみに、この辺りの動作は上記test-lib, test-appにおいて、1. defhoge
のprogn
をeval-when
に置き換える、2. pushnew
をpush
で置き換える、3. *load-kind*
を1に設定する、としてみると確認できます("1 from LIB"が2回出ます)。
記事を書くにあたり、参考にClozure CLのdefstruct
の実装を見てみたのですが、グローバルな環境への登録はあくまでロード時に行っており(%defstruct-do-load-time
)、コンパイル時にはレキシカルな環境&environment env
に一時的に登録することで副作用を避けているようです(define-compile-time-structure
)。マクロ展開が「純粋に関数的」な動作をするようにかなり慎重に作られている様子が伺えます。注意ですが、まだ&environment
を理解し切れていないので嘘を言っているかもしれません。
まとめ:恐しさについて改めて
マクロ展開時に副作用を起こすことの恐ろしさは、原因を特定しにくいバグにつながる、というところにつきます。
マクロ展開時の副作用の結果は環境には残るため、Lispの利点であるインクリメンタルな開発の最中にはまず気づきません。さらに、テストを書いてクリーンな環境で実行していてもまだ気づかないケースも多いです。これは、上記のps-experimentのdefstruct
サブセットのように、自身では使わない外向けに提供する機能で起こりやすいです。そしてある日、実行条件に応じて結果が変わるような再現しにくいバグに遭遇します。
バグの原因特定を困難にする典型的な要因である、発見までに時間がかかることと、再現条件が分かりにくいことという両方を満たすわけです。自分はこのバグに遭遇してから見当違いの方向にも走りつつ数日苦しみました。
ということで、マクロ展開時の副作用には敏感になりましょう、と釈迦に説法をしたところで終わりにします。
*1:Parenscriptの仕様と関係なく勝手にハマった部分も多々ありますが、、とりあえず関係ある部分の紹介です
*2:eval-whenがdefpsmacro内で呼ばれていないのは、それはそれで問題なのですが、外付けで対処可能なため傷は浅いです。eval-when自体について参考になるのはこの辺り「macros - Eval-when uses? - Stack Overflow」でしょうか。なお、Parenscriptのこのコミット(リンク)でmasterは修正されていますが、quicklispの参照しているhttp://common-lisp.net/project/parenscript/release/parenscript-latest.tgzに反映されていないようです
*3:本題と関係ない調査メモ。HyperSpecによると、*compile-file-pathname*はcompile-file関数の実行中のみファイルパスが設定され、それ以外はnilにセットされるもののようです。また、macroexpand-progress-fun自体は、*macroexpand-hook*用のhook関数を返します。切り出したコードの少し下を見ると、defpackageマクロの展開時であることが内部関数show-packageを呼び出す条件の一つになっています。以上を合わせて、コンパイル時のみパッケージ名を(重複なく)出力する動作を実現しているようです。
*4:HyperSpecにあるexpansion functionの説明では"The value of the last form executed is returned as the expansion of the macro"と記述されています。
*5:上の例では、ここでコンパイルさせるためにtest-ps-eval-orderライブラリ側にわざとらしく変更を加えています :)
SBCLとCCLの違い:defstructのincludeにおけるスロット名の比較方法
Common Lispの構造体定義マクロdefstruct
では、:include
で別の構造体のスロット名やその初期値を引継ぐことができます。このとき、下記の(a 100)
のように初期値を上書きできます。
CL-USER> (defstruct parent (a 10) (b 20)) PARENT CL-USER> (defstruct (child (:include parent (a 100))) c) CHILD CL-USER> (make-child) #S(CHILD :A 100 :B 20 :C NIL)
:include
内で存在しないスロット名を指定すると当然エラーになるわけですが、このときのスロット名の比較(存在するかを判定するための)の仕方がSBCLとClozure CL (CCL) *1で異なるようだったのでメモ。
このRoswellスクリプトを走らせると、SBCLではエラーなく動きますが、
#S(TEST-STRUCT2 :SLOT1 100 :SLOT2 NIL)
CCLではslot1
なんて知らんと言われてエラーになります(CLISPもこのパターンでした)。
Error: TEST-STRUCT has no SLOT1 slot, in (:INCLUDE TEST-STRUCT (SLOT1 100))
CCLで動かすためには、初期値を上書きする部分でpack-a
にintern
されたslot1
シンボルを指定する必要があります(pack-a
の定義でslot1
もexportするなど)。なお、exportしておく分にはSBCLでも問題なく動くので、必要な場合はexportしておくのが正しいと思います。
include
において、SBCLではパッケージに依存しない形でスロット名を比較しており、CCLではパッケージに依存した形で比較しているようです。…で終わるのも悲しいので、それぞれの該当部分のソースもほんの少し見てみます。
まずSBCLのソースです。手元にあったバージョン1.2.15を見てみました。エラーメッセージから該当箇所は簡単に見つかります。:test #'string=
と比較方法を指定しているので、確かにパッケージを含まないシンボル名のみで比較しているようです。
;; L768~@src/code/defstruct.lisp (defun frob-dd-inclusion-stuff (dd) ;; ...(略)... (flet ((included-slot-name (slot-desc) (if (atom slot-desc) slot-desc (car slot-desc)))) (mapl (lambda (slots &aux (name (included-slot-name (car slots)))) (unless (find name (dd-slots included-structure) :test #'string= :key #'dsd-name) (error 'simple-program-error :format-control "slot name ~S not present in included structure" :format-arguments (list name))) ;; ...(略)...
次はCCLです。バージョンは1.9です*2。同じくエラーメッセージから探してみます。named-ssd
なるマクロでスロットが存在するかを探していますが、最終的には下のようにeq
で比較をしています。ということで、確かにパッケージを含む形で比較を行っているようです。
;; L42~@lib/defstruct-lds.lisp (defmacro defstruct (options &rest slots &environment env) ;; ...(略)... (while slots (if (atom (car slots)) (setq name (%car slots) args ()) (setq name (%caar slots) args (%cdar slots))) (unless (symbolp name) (signal-program-error $XNotSym name)) (unless (setq ssd (named-ssd name slot-list)) (error "~S has no ~S slot, in ~S" (sd-name sub-sd) name (cons :include include))) ;; ...(略)...
;;L74@lib/defstruct-macros.lisp (defmacro named-ssd (name slot-list) `(assq ,name ,slot-list)) ;; L402~@compiler/optimizers.lisp (define-compiler-macro assq (item list) (let* ((itemx (gensym)) (listx (gensym)) (pair (gensym))) `(let* ((,itemx ,item) (,listx ,list)) (dolist (,pair ,listx) (when (and ,pair (eq (car ,pair) ,itemx)) (return ,pair))))))
SBCLはスロット名なんてパッケージ関係ないよねという考え方…かと思いきや、with-slots
やslot-value
ではやっぱりパッケージを考慮しているので、なんだか半端です。この辺りも考え合わせると、SBCLのやり方が特殊という感じがします。どういった考えなんでしょう。
Common Lispでマクロ展開時エラーをテスト
前回記事の「Parenscriptで少し遊んで見る (5)defstruct編 - eshamster’s diary」ではdefstruc.psマクロ内で展開時のエラー処理を入れたので、テストも一緒に作っています。このときに、マクロ展開時のエラーをテストする方法が意外と見つからなかったのでメモ。探し方が悪いのか、そんなの当たり前でしょということで誰も書いていないのか。
CL-USER> (defmacro throw-error-macro (x) (if (numberp x) `(+ ,x 10) (error 'type-error))) THROW-ERROR-MACRO
特に意味のないマクロですが、数値型を受けとると10を足す式を返し、それ以外の型を受けとると(展開時に)type-error
を投げます。この挙動をテストしたい訳ですが…
CL-USER> (ql:quickload :prove :silent t) (:PROVE) CL-USER> (prove:is-error (throw-error-macro nil) 'type-error) ; ; compilation unit aborted ; caught 1 fatal ERROR condition ; caught 1 ERROR condition ; Evaluation aborted on #<SIMPLE-ERROR "unbound condition slot: ~S" {1005EC7663}>.
というように、prove:is-error
が呼び出される前のコンパイル段階でエラーになるため、prove:is-error
でエラーを捕捉できません。マクロを実行時に評価できれば良いわけですが、実行時評価となればeval
の出番です。
CL-USER> (prove:is-error (eval '(throw-error-macro nil)) 'type-error) ✓ (EVAL '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {1003C91FE3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {1003C91FE3}>, EXPECTED: TYPE-ERROR>
できました。一々evalやクォートを書くのが気持ち悪い場合、以下のようにprove-macro-expand-error
マクロを作成します。
CL-USER> (defmacro prove-macro-expand-error (code expected-error) `(prove:is-error (eval ',code) ,expected-error)) PROVE-MACRO-EXPAND-ERROR CL-USER> (prove-macro-expand-error (throw-error-macro nil) 'type-error) ✓ (EVAL '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {10044945E3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {10044945E3}>, EXPECTED: TYPE-ERROR>
言われてみれば当たり前な気がしますが、個人的にはeval
を使う機会がなかったので中々出てこない発想でした。
ちなみに、元々は「リードマクロ入門、の10分の1歩ぐらい後か前 - eshamster’s diary」のコメントで教えて頂いたREPL環境でのリードマクロの試用方法から思い至ったため、(format nil "~S" ...)
で文字列化してread-from-string
をしてeval
するという無駄なことをしていました。一方で、リードマクロのエラーをテストしたい場合はread-from-string
を使えば良さそうです。
追記:macroexpand
g000001さんからmacroexpand(-1)
の方が一般的とのコメントを頂きました。eval
は汎用的過ぎるので、可能な限りより用途の限定されたもので代用するのが適切だと思いますが、macroexpand(-1)
はまさにぴったりの用途ですね。Lispを勉強し始めてからは前より人のソースを見ることが増えましたが、まだまだ勉強不足でした。
ということで、macroexpand
版です。見た目としてはeval
がmacroexpand
に置き代わっただけです。
CL-USER> (prove:is-error (macroexpand-1 '(throw-error-macro nil)) 'type-error) ✓ (MACROEXPAND-1 '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {10060A87D3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {10060A87D3}>, EXPECTED: TYPE-ERROR> ;; マクロ化 CL-USER> (defmacro prove-macro-expand-error (code expected-error) `(prove:is-error (macroexpand ',code) ,expected-error)) PROVE-MACRO-EXPAND-ERROR CL-USER> (prove-macro-expand-error (throw-error-macro nil) 'type-error) ✓ (MACROEXPAND '(THROW-ERROR-MACRO NIL)) is expected to raise a condition TYPE-ERROR (got #<TYPE-ERROR {1003246EA3}>) T #<PASSED-TEST-REPORT RESULT: T, GOT: #<TYPE-ERROR {1003246EA3}>, EXPECTED: TYPE-ERROR>
Parenscriptで少し遊んで見る (5)defstruct編
Parenscript(PS)用にdefstructのサブセットを作った話です。例によってParenscript拡張の実験場、ps-experimentプロジェクトで実装を試みています。今回の記事時点のタグblog-play-ps-5
をつけています。
前書き:Parenscript拡張の方針
ここまでと今回の内容を振り返ってみると、大体次のような方針で拡張を進めているようです(後付け)。
- Common Lispとして書ける部分はそれなりにCommon Lispらしく
- 理想を言えば、Common Lispコードとしてもそのまま動く
- JavaScriptべったりなところはむしろよりJavaScriptらしく
- xx.jsのようなライブラリに依存する部分
前者はdefun編や今回のdefstruct編、後者はドット記法編やキャメルケース編です。前者の「それなり」の範囲は感覚的になんとなく許せない範囲です。
今回で言うと、基本的なデータ構造がJSべったりなのはちょっと…と感じたわけです。
defstruct.psの実装方針
defstructを全面的にカバーするのはオーバースペックなので、defstruct.psの目標を次のように設定しました。
- 作成時にスロットの初期化ができる
- 型判定ができる
- アクセサを生成する
- 継承ができる
見ての通りdefstructの仕様からすると超のつくサブセットです。まずは想定範囲内では使えそうというところです。
使い方
できあがったものを上記の目標に沿って一通り動かしてみます。出力はprintの横に逐次コメントで書いているので省略します。
展開結果
(print (pse:with-use-ps-pack (:this)))
で見られる展開結果のうち、childの部分だけ抜粋。アクセサはPS用のマクロとして別に管理されているため、JSコードとしては見えません。
function child() { this.a = 10; this.b = null; return this.c = 30; }; function makeChild() { var _js4 = arguments.length; for (var n3 = 0; n3 < _js4; n3 += 2) { switch (arguments[n3]) { case 'a': a = arguments[n3 + 1]; break; case 'b': b = arguments[n3 + 1]; break; case 'c': c = arguments[n3 + 1]; }; }; var a = 'undefined' === typeof a ? 10 : a; var b; var c = 'undefined' === typeof c ? 30 : c; var result = new child(); result.a = a; result.b = b; result.c = c; return result; }; function childP(obj) { return (obj instanceof child); }; (function () { function tempCtor() { return null; }; tempCtor.prototype = parent.prototype; child.superClass_ = parent.prototype; child.prototype = new tempCtor(); return child.prototype.constructor = child; })();
Parenscriptのキーワード引数の実装が少々不恰好ですが、現状ではJSコードへのコンパイル段階でどの関数を呼んでいるかを知るすべがないので、止むを得ないというところです。
実装
ps環境下でのdefstruct
一部を切り出し。まずは、(ps:ps (defstruct test (a 10) b))
のように、ps環境下でdefstructを利用可能にするためのコード。一番下の(defpsmacro defmacro ...)
が本体で、その上に補助関数をずらっと並べています。
見ての通りparse-defstruct-xxx
がひたすら並んでいます*1。また、生成されたJavaScriptを見て文法エラーを見つけるのは辛そうなので、パース時のエラー処理を(それなりに)入れています。
JavaScript側の継承を実現する上では「Google流 JavaScript におけるクラス定義の実現方法」を参考にしました。最初はObject.setPrototypeOfを使ったお手軽な方法を使おうと考えていました。一連の流れをOpera(Chromiumエンジン)のコンソールで再現すると以下のようになります。
> function Parent () { this.a = 10; this.b = 20 }; undefined > function Child () { Parent.call(this); this.c = 30; }; undefined > Object.setPrototypeOf(Child.prototype, Parent.prototype); Child {} > var child = new Child(); undefined > child instanceof Parent; true
が、上記のサイトには互換性に問題ありと書かれていて、どうせ古いIEのことだろうとタカをくくっていたところ、テストに利用しているcl-javascriptで動きませんでした…。ということで、上記のコード(同サイトで紹介されているgoog.inherit実装のParenscript版)になりました。
また、スロット周りの継承を実現するために*ps-struct-slots*
というグローバルなハッシュを用意しています。スロットそのものの継承だけであればJavaScriptのcall関数で十分ですが、CLライクなアクセサの定義を行なうために必要になります。
トップレベルでのdefstruct.ps
「使い方」のRoswellスクリプトの様に、トップレベルでPS用の構造体定義を可能にするためのマクロdefstruct.ps
を生成します。直接には3,4行目が該当のものです。
2015/11/30追記:よろしくないバグを見つけたので修正。あえて古いコードもコメントで残しましたが、On Lispにもある「値を返す他には周囲の世界に影響しようとすべきではない」の原則に反していました。元のコードでは、例えばdefun.psを使ったライブラリxyzを(ql:quickload :xyz)しても、関数が登録されていませんでした。さらに困ったことに、稀に登録されてることがありました…。この辺りの内部的な挙動はまだ理解し切れていませんが。
2015/12/5追記:defstruct.ps内でもregister-defstruct-slotsを呼ばないと上の追記と同じ問題が起こることが分かったのでこのコミットで修正。ちょっと汚いのでどうにかならないものか…。ちなみに、eval-whenは試行錯誤の痕跡でただの消し忘れです。
register-ps-func
関数の詳細は第4回参照ですが、PSコードを出力する関数をグローバルに登録するものです。登録した関数はwith-use-ps-pack
内(上記実行例参照)で呼び出されてPSコードを出力します(最終的にこのPSコードがJSコードを出力します)。
defstruct.ps
で構造体hogeを定義すると、hogeを定義するPSコードを出力する関数がこのregister-ps-func
で登録されます。この辺りはdefun.ps
やdefvar.ps
でも共通のパターンであるため、def-ps-definer
としてマクロ化しました。
できていないもの
- defstructの機能色々
- 必要になったものから順次というスタンスです
- 再定義時の動作
Parenscript関連記事
Lisp-Parenscript カテゴリーの記事一覧 - eshamster’s diary
*1:S式≒リストのパースは本当に気軽にできますね