HOME Common Lisp code 書き込む

マクロを使った双方向計算


1. はじめに

一般にプログラム言語の関数は引数から返り値への一方向の計算をします。一方、物理学などで登場する公式は 右辺と左辺を等号で結んだ関係式で表されます。 例えば、理想気体の状態方程式 *
PV = nRT
は、圧力(P), 体積(V), モル数(n), 気体定数(R), および温度 (T) の関係を表したもので、 4つの変数の値が決まれば上の関係式から残りの変数の値が求まります。 関数でこれらの値を求めるためには次のように求める変数ごとに別個の関数を作る必要があります。
(defun igas-p (v n d)
  (/ (* 8.31415 n d) v))
(defun igas-v (p n d)
  (/ (* 8.31415 n d) p))
(defun igas-n (p v d)
  (/ (* p v) 8.31415 d))
(defun igas-t (p v n)
  (/ (* p v) 8.31415 n))
これはいささか不便で、4つの変数の値が決まれば残りの変数の値を計算してくれる 双方向あるいは無方向の”計算素子”があると便利です。

いわゆる双方向計算は双方向計算素子のネットワーク上で、”制約”を伝播させることによって 実現されますが、マクロを使うとほぼ同等なことが容易に実現できます。 マクロは式を生成し、その式を評価して値を得るので、双方向計算にはうってつけの手法です。 次の様なマクロを書くことによって別個の関数を作ることなしに任意の変数の値を求めることが出来ます。

; check if given list has single element
(defun single (obj)
   (and (car obj) (not (cdr obj))))

; group items in the list such as (a 1 b 2 c 3) --> ((a 1) (b 2) (c 3))
(defun group (source n)
  (if (zerop n) (error "zero length"))
    (labels ((rec (source acc)
	       (let ((rest (nthcdr n source)))
		 (if (consp rest)
		     (rec rest (cons (subseq source 0 n) acc))
		   (nreverse (cons source acc))))))
      (if source (rec source nil) nil)))

;a macro of bidirectional calculations for ideal gases
(defmacro igas0 (&rest av)  
  (let* ((parm (group av 2))
	 (unknown (set-difference '(p v n d) (mapcar #'car parm)))
	 (r 8.31415))
    (unless (single unknown)
      (error "Invalid unknown."))
    `(let ((*WARN-ON-FLOATING-POINT-CONTAGION* nil))
       (let ,parm
	 ,(case (car unknown)   
	    (p `(/ (* ,r n d) v))
	    (v `(/ (* ,r n d) p))
	    (n `(/ (* p v) ,r d))
	    (d `(/ (* p v) ,r n)))))))

;;; 展開形
>(pprint (macroexpand-1 '(igas0 p 1.0e5 n 1.0 d 300)))
(let ((*WARN-ON-FLOATING-POINT-CONTAGION* nil))
  (let ((p 100000.0) (n 1.0) (d 300))
    (/ (* 8.31415 n d) p)))

;;; 実行例
>(igas0 p 1.0e5 n 1.0 d 300)
0.02494245
(igas0 p 1.0e5 n 1.0 d 300)
のように、値が既知の変数名とその値を並べて引数として与えることで、値が未定の変数の値を計算します。 この場合は v の値を計算しその値は 0.02494245 (m3) となります。 ちなみに、ここで 温度を表すのに d を使っているのは t が 真を表す Common LISP の予約語だからです。

2. マクロ defformula

上に示した igas0 のようなマクロを自動的に生成するマクロがあると便利です。そのようなマクロの 例としてマクロ defformula を書いてみました。使い方は以下の通りです。
  1. ソースコードをダウンロードして、 LISP 処理系でコンパイルし、ロードします。
  2. (defformula [マクロ名] [式])
    で双方向計算マクロを定義します。 ここで、
    [マクロ名]
    定義するマクロ名。
    [式]
    マクロで使う式、左辺から右辺を引いて、 値が恒常的に 0 になるように変形してください。 例えば、PV = nRD は (- (* p v) (* n r d)) と変形します。 [式] には以下の関数が使えます。
    +, -, *, /, exp, expt, log, sqrt,
    sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh

    例:
    ; 理想気体の状態方程式, PV = nRT
    (defformula igas (- (* p v) (* n 8.31451 d)))
    
    ; van der Waals の実在気体の状態方程式,  (P + an2/V2)(V - nb) = nRT**
    (defformula rgas (- (* (+ p (/ (* a (expt n 2)) (expt v 2))) (- v (* n b))) (* n 8.31451 d)))
    
  3. defformula で定義したマクロを
    ([マクロ名] &rest [値が既知の変数])
    の様にして使います。未知の変数の値と変数名の2値を返します。 ここで、
    [マクロ名]
    defformula で定義したマクロ名。
    [値が既知の変数]
    マクロの引数として与える変数名、値のリスト。 変数名と値がペアになって並んでいれば順序はかまいません。

    なお、