- Common Lispの
*features*リストとリーダーマクロ#+/#-を使うと、コンパイル前の読み取り段階で環境ごとにコードの読み込みを切り替えられます。 - SLIME接続中のみ
:SWANKが*features*に存在するため、#+swankでテストコード、#-swankで(main)呼び出しを分けて書けます。 - これにより、AtCoderへの提出時にテストコードを手動削除する作業がなくなり、同じファイルのままローカルとAtCoderで動作が自動的に切り替わります。
1. *features* とリーダーマクロ
AtCoderにCommon Lispを貼るとき、Emacsで書いたテストコードを削除したり、メイン関数の実行を追加したりするのが、地味に面倒に感じます。
そこで、実行環境によってコードを変えるマクロを設定しました。
Common Lispには *features* というリストがあり、実行環境の特徴を示すキーワードが入っています。
*features*
;; => (:SWANK :OS-MACOSX :BSD :DARWIN :MACH-O :SBCL ...)Code language: Lisp (lisp)
この*features*は、リーダーマクロ #+ #- と組み合わせ使います。#+キーワードは「そのキーワードが *features* に含まれるときだけ、直後の式を読み込む」リーダーマクロで、#-キーワード は逆で、含まれないときだけ読み込みます。
コンパイル前の読み取り段階で処理されるため、条件が偽のとき式はコードとして存在しません。(when ...) で囲むのとは根本的に違い、実行時にすら評価されません。
C言語でのプリプロセッサマクロ #ifdef SLIME ... #endif に相当します。
1.1. SLIMEとAtCoderで *features* は何が違うか
環境による差分を調べるために、まずAtCoderの *features* を確認します。

AtCoderのコードテストで *features* を出力すれば、確認できます。
その値を *at-coder-features* として定義しておきます。
(defparameter *at-coder-features*
(list :QUICKLISP :ASDF3.3 :ASDF3.2 :ASDF3.1
:ASDF3 :ASDF2 :ASDF :OS-UNIX
:NON-BASE-CHARS-EXIST-P :ASDF-UNICODE
:ARENA-ALLOCATOR :X86-64 :GENCGC :64-BIT
:ANSI-CL :COMMON-LISP :ELF :IEEE-FLOATING-POINT
:LINUX :LITTLE-ENDIAN
:PACKAGE-LOCAL-NICKNAMES :SB-LDB
:SB-PACKAGE-LOCKS :SB-THREAD :SB-UNICODE
:SBCL :UNIX :SB-MPFR :SB-GMP-5.1
:SB-GMP-5.0 :SB-GMP))Code language: Lisp (lisp)
あとは、リストの差分を比較します。intrinsic-members は「aにあってbにないもの」を返します。
(defun intrinsic-members (a b)
(loop for x in a
unless (member x b)
collect x))Code language: Lisp (lisp)
これをSLIMEのREPLで実行すると、両環境の差分が出ました。
;; AtCoderにあってSLIMEにないもの
(intrinsic-members *at-coder-features* *features*)
;; => (:ELF :LINUX :SB-MPFR :SB-GMP-5.1 :SB-GMP-5.0 :SB-GMP)
;; SLIMEにあってAtCoderにないもの
(intrinsic-members *features* *at-coder-features*)
;; => (:SWANK :OS-MACOSX :BSD :DARWIN :MACH-O :SB-CORE-COMPRESSION)Code language: Lisp (lisp)
私の環境では、特徴的なフィーチャーフラグは、:SWANK でした。
これはSLIMEのバックエンドサーバーで、SLIME接続中のみロードされます。
つまり、Emacsの編集中でのみ存在するフラグなので、AtCoderのSBCL環境には存在しません。
| 環境 | #+swank ブロック | #-swank ブロック |
|---|---|---|
| SLIME(ローカル) | 実行される | スキップされる |
| AtCoder | スキップされる | 実行される |
2. テストと本番を同じファイルに書く
main の呼び出しを #-swank に書きます。
こうすれば、SLIMEでのロード時に実行されません。
#-swank
(main)Code language: Lisp (lisp)
よく実行して、入力待ちになっていました。
私は、テストをParachuteで書いているので、この読み込み・定義・実行を prognでまとめて #+swank ブロックに入れました。
(defun answers (queries k items)
;; 実装
)
#+swank
(progn
(ql:quickload :parachute)
(parachute:define-test answers-test
(parachute:is equal '(1 1 2 3 2 3 1 2)
(answers (list 1 1 2) 3 (list 1 2 3 4 5 6 7 8)))
(parachute:is equal '(30 2 18 21 7 9 29 19 27 3)
(answers (list 20 26 3 14 4 4 9) 30
(list 31 9 21 23 97 99 30 79 57 3))))
(parachute:test 'answers-test))Code language: Lisp (lisp)
Emacsで C-c C-k するとファイルをコンパイル・ロードし、#+swank ブロックが実行され、SLIME上にテスト結果が出ます。
AtCoderに貼って実行すると :swank がないので #+swank ブロックは丸ごと読み飛ばされ、#-swank の (main) だけが走ります。
テストコードを手動で消す作業は不要になりました。
3. 【補足】eval-when との比較
ちなみに、同じことを eval-when + find-package で書くとこうなります。
(eval-when (:load-toplevel)
(when (find-package :swank)
(ql:quickload :parachute)
(parachute:define-test answers-test ...)
(parachute:test 'answers-test)))Code language: Lisp (lisp)
意味は同じですが、#+swank の方が短く、テストブロックだとひと目でわかります。eval-when は「いつ評価するか」という制御に使うもので、環境の切り替えには #+ / #- の方が意図に合っています。
3.1. 【補足】AtCoder固有のフィーチャーも活用できる
AtCoder環境には :SB-GMP と :SB-MPFR が入っています。
GMP(GNU Multiple Precision Arithmetic Library)による多倍長整数と、MPFRによる高精度浮動小数点が使える状態です。
ローカルにインストールしていなくても、AtCoder上では動きます。
問題によっては #+sb-gmp でGMPを前提にした実装を書き、ローカルではフォールバックに切り替えるという使い方もできます。
4. まとめ
フィーチャーフラグは実行前の読み取り段階で解決されるため、無効な環境でコードが存在しなくなります。
提出ファイルにテストコードが残ったまま動く、というのがこのアプローチの実用的なポイントです。