y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

defvarとdefparameterの使い分け事例

この文章は、 Lisp Advent Calendar 2020 - Qiita の記事として 12/31 に書かれました。

defvardefparameter の違い

Common Lisp にはスペシャル変数を定義する方法として、 defvardefparameter があります。 この二種をどう使い分けるかについてについては色々と記事があります。例えば 逆引きCommon Lispでは以下のようになっています:

defvar は初期値なしでもスペシャル変数を定義でき、初期値を指定した場合は、その変数が未定義だった場合のみ初期値を代入します。
一方、defparameterは、初期値を必ず指定する必要があり、すでにその変数に値が存在するかどうかにかかわらず初期値を設定します。

Common Lisp HyperSpec にも使い分け例がありますが、ここでは筆者が経験した二者の使い分け事例を書いてみます。

事例1: 中間結果を保持する変数には defvar を使った方がよさそう

とても重い処理をする関数があった場合、その結果をどこかの変数に格納しておいて使い回そうとすることがよくあります。 以下のコードを考えてみます:

(in-package :cl-user)

(defparameter *とても重い処理の結果*
  (make-hash-table))

(defun とても重い処理 ()
  ;; `*とても重い処理の結果*' が空の場合のみ、値を作る
  (when (zerop (hash-table-count *とても重い処理の結果*))
    (setf *とても重い処理の結果*
          (loop with table = (make-hash-table) ; このぐらいの処理は全然重くないが・・
             for i from 1 to 9999
             do (setf (gethash i table) i)
             finally (return table))))
  ;; 計算済の値があれば、それを使い回す
  *とても重い処理の結果*)

;; この他に `とても重い処理' を使う関数がたくさんあるとする。

上では、 関数 とても重い処理 は、変数 *とても重い処理の結果* が空の場合にのみ hash-table を作ります。 変数 *とても重い処理の結果* に値がある場合は、それを使い回しています。

さて、開発中はコードを Load し直すということはよくあると思います。 関数 とても重い処理 以外のコードを書き直したあと、深く考えずに defparameter が含まれたファイルを丸ごと load するわけです:

(load "とても重い処理のコード")

すると、 defparameter は Load 前の値を上書きしますから、以前の値は失われます:

CL-USER> (hash-table-count *とても重い処理の結果*)
0

結果として、 関数 とても重い処理 は再び値を計算し直し、変数 *とても重い処理の結果* を作り直してしまいます。 関数 とても重い処理 を書き直したのなら計算結果を再計算する必要があると思われますが、そうでないなら再計算するのは無駄です。

筆者は、 こんな調子で何度も何度も中間結果を失ってきました・・

このような中間結果を保持するような目的で使う変数には defvar を使った方がいいと思います。

(defvar *とても重い処理の結果*
  (make-hash-table))
;; 他は同一

これならば、 load を経ても値は維持されます。

一方で、関数 とても重い処理 を書き直したのなら計算結果を再計算するために、逆に変数 *とても重い処理の結果* を手動で消す必要があります。 これには、手動で消すか、エディタのサポート (Slime なら C-M-x slime-eval-defun で可能)が使えます。

事例2: 設定読み込み時になどに上書きしそうな変数には defvar を使った方がよさそう

設定読み込みで上書きしそうな変数にも defvar を使った方がいいかなと思っています。 以下のコードを考えてみます:

(defparameter *some-host* "localhost")

(defun load-configuration ()
  (setf *some-host* "example.com"))

変数 *some-host* には初期値として使えそうな値を保持させつつ、関数 load-configuration を呼ぶことで上書きできるようにしてあります。

さて、開発中はコードを Load し直すということはよくありますから、この defparameter が含まれたファイルを丸ごと load してしまいがちです。 すると当然、変数 *some-host* は初期化され・・ 筆者はなんで値が消えたの???と狂乱する わけです。

このような場合にも、 defvar を使って値を保持した方がいいような気がします。

まとめ

筆者は、 一つ前の記事 と合わせて、 defparameterdefvar に置き換える作業をして、今の所 defvar を以下の変数に使っています:

  • 設定読み込みで値を変える変数
  • 絶対に LET を忘れてはいけない変数
  • 中間結果を保持する変数

そういうことをしていると、 defparameter の使用箇所がどんどん減って・・ defparameter の用途が 初期値から値を変化させる機会が(ほとんど)ない変数 だけになってしまいました。

そんなものなんでしょうかね?ご意見伺いたいです。