#+ #- 【Common Lisp】
AtCoderのコードにテストを埋め込む
(フィーチャーフラグ)

  • 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* を確認します。

1.1. SLIMEと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. まとめ

フィーチャーフラグは実行前の読み取り段階で解決されるため、無効な環境でコードが存在しなくなります。
提出ファイルにテストコードが残ったまま動く、というのがこのアプローチの実用的なポイントです。