2017-01-01 2 views
8

私はCommon LispをPractical Common Lispから学んでいます。これは、第24章でバイナリファイルを読み書きするためのヘルパー関数の例を持ってここに1つの例を示しますCommon Lispで同様の関数を書くには?

(defun read-u2 (in) 
    (+ (* (read-byte in) 256) (read-byte in))) 

私は同様に、二進数の他の種類を読み取るための関数を書くことができます。しかし、私はDRYの原則に違反すると思っていました。また、これらの関数は似ているので、マクロを使って関数を生成しようとしました。

(defmacro make-read (n be) 
    `(defun ,(intern (format nil "READ~d~:[L~;B~]E" n be)) 
     (&optional (stream *standard-input*)) 
    (logior ,@(loop for i from 0 below n collect 
       `(ash (read-byte stream) 
         ,(* 8 (if be (- n 1 i) i))))))) 

(defmacro make-read-s (n be) 
    `(defun ,(intern (format nil "READ~d~:[L~;B~]E-S" n be)) 
     (&optional (stream *standard-input*)) 
    (let ((a (,(intern (format nil "READ~d~:[L~;B~]E" n be)) stream))) 
     (if (zerop (logand a ,(ash 1 (1- (* 8 n))))) 
     a 
     (logior a ,(ash -1 (* 8 n))))))) 

(defmacro make-write (n be) 
    `(defun ,(intern (format nil "WRITE~d~:[L~;B~]E" n be)) 
     (n &optional (stream *standard-output*)) 
    (setf n (logand n ,(1- (ash 1 (* 8 n))))) 
    ,@(loop for i from 0 below n collect 
     `(write-byte (ldb (byte 8 ,(* 8 (if be (- n 1 i) i))) n) 
        stream)))) 

(eval-when (:compile-toplevel :load-toplevel :execute) 
    (dolist (cat '("READ" "READ-S" "WRITE")) 
    (dolist (be '(nil t)) 
     (dolist (n '(1 2 4 8)) 
     (eval `(,(intern (format nil "MAKE-~a" cat)) ,n ,be)))))) 

これは機能します。 1、2、4、および8のサイズの符号なしおよび符号付き整数を読み書きするための関数を生成します.SLIMEはそれを理解します。しかし、より良い方法があるのだろうかと思います。

Common Lispで同様の機能をたくさん作成するにはどうすればよいですか?

答えて

9

このコードにはいくつかの問題がありますが、マクロ生成機能を持つ一般的な方法は問題ありません。

命名彼らは何かを作る機能が、関数を定義するマクロではありませんので、マクロは、make-...命名するべきではありません。

コード生成

EVAL-WHEN ... EVALコードは本当に悪いですし、この方法を使用すべきではありません。

より良い方法は、関数定義でprognに展開するマクロを書くことです。

EVALを使用したければ、マクロを生成するコードを書く必要はなく、単純にコード生成関数をコードします。しかし、私はEVALを使用したくない、私はコンパイラのためのコードを直接作成したい。マクロを生成するコードがある場合は、EVALは必要ありません。

EVALは、コードがコンパイルされることが明確でないため、実装に依存します。また、コンパイル時およびロード時に評価が行われます。コンパイル時に関数をコンパイルし、ロード時にのみロードする方が良いでしょう。また、ファイルコンパイラは、評価された関数の可能な最適化を見落とす可能性があります。

(defmacro def-reader/writer-functions (cat-list be-list n-list) 
    `(progn 
    ,@(loop for cat in cat-list append 
      (loop for be in be-list append 
        (loop for n in n-list 
         collect `(,(intern (format nil "DEF-~a-FUN" cat)) 
            ,n 
            ,be)))))) 

今、私たちはすべての機能を生成するために、マクロの上に使用することができます:

(def-reader/writer-functions 
("READ" "READ-S" "WRITE") 
(nil t) 
(1 2 4 8)) 

することができます代わりに、私たちは別のマクロを定義してから、我々は、後でそれを使用EVAL-WHEN ... EVAL

(defmacro def-read-fun (n be) 
    `(defun ,(intern (format nil "READ~d~:[L~;B~]E" n be)) 
      (&optional (stream *standard-input*)) 
    (logior ,@(loop for i from 0 below n collect 
        `(ash (read-byte stream) 
          ,(* 8 (if be (- n 1 i) i))))))) 

(defmacro def-read-s-fun (n be) 
    `(defun ,(intern (format nil "READ~d~:[L~;B~]E-S" n be)) 
      (&optional (stream *standard-input*)) 
    (let ((a (,(intern (format nil "READ~d~:[L~;B~]E" n be)) stream))) 
     (if (zerop (logand a ,(ash 1 (1- (* 8 n))))) 
      a 
     (logior a ,(ash -1 (* 8 n))))))) 

(defmacro def-write-fun (n be) 
    `(defun ,(intern (format nil "WRITE~d~:[L~;B~]E" n be)) 
      (n &optional (stream *standard-output*)) 
    (setf n (logand n ,(1- (ash 1 (* 8 n))))) 
    ,@(loop for i from 0 below n collect 
      `(write-byte (ldb (byte 8 ,(* 8 (if be (- n 1 i) i))) n) 
          stream)))) 

ここでの展開をご覧ください:

CL-USER 173 > (pprint (macroexpand-1 '(def-reader/writer-functions 
             ("READ" "READ-S" "WRITE") 
             (nil t) 
             (1 2 4 8)))) 

(PROGN 
    (DEF-READ-FUN 1 NIL) 
    (DEF-READ-FUN 2 NIL) 
    (DEF-READ-FUN 4 NIL) 
    (DEF-READ-FUN 8 NIL) 
    (DEF-READ-FUN 1 T) 
    (DEF-READ-FUN 2 T) 
    (DEF-READ-FUN 4 T) 
    (DEF-READ-FUN 8 T) 
    (DEF-READ-S-FUN 1 NIL) 
    (DEF-READ-S-FUN 2 NIL) 
    (DEF-READ-S-FUN 4 NIL) 
    (DEF-READ-S-FUN 8 NIL) 
    (DEF-READ-S-FUN 1 T) 
    (DEF-READ-S-FUN 2 T) 
    (DEF-READ-S-FUN 4 T) 
    (DEF-READ-S-FUN 8 T) 
    (DEF-WRITE-FUN 1 NIL) 
    (DEF-WRITE-FUN 2 NIL) 
    (DEF-WRITE-FUN 4 NIL) 
    (DEF-WRITE-FUN 8 NIL) 
    (DEF-WRITE-FUN 1 T) 
    (DEF-WRITE-FUN 2 T) 
    (DEF-WRITE-FUN 4 T) 
    (DEF-WRITE-FUN 8 T)) 

次に、各サブフォームが関数定義に展開されます。

このように、コンパイラはコンパイル時にすべてのコードを生成するためにマクロを実行し、コンパイラはすべての関数のコードを生成できます。私は&optionalパラメータを使用したくない場合も最低レベルの機能で

効率/デフォルト

。デフォルトの呼び出しは動的バインディングから値を取得し、さらに悪いことに*standard-input*/*standard-output*は、READ-BYTEまたはWRITE-BYTEが動作するストリームではない可能性があります。どの実装でも、標準の入出力ストリームをバイナリストリームとして使用することはできません。

LispWorks:

CL-USER 1 > (write-byte 13 *standard-output*) 

Error: STREAM:STREAM-WRITE-BYTE is not implemented for this stream type: #<SYSTEM::TERMINAL-STREAM 40E01D110B> 
    1 (abort) Return to level 0. 
    2 Restart top-level loop. 

私もインライン化されるように生成されたすべての関数を宣言することもできます。

タイプ宣言は、もう1つ考えられます。

要約:EVALは使用しないでください。

+0

なぜ「&optional」を使用しないのですか?それが効率のためであれば、関数がインライン化されている場合でも適用されますか? – nisekgao

+0

@nisekgao:私の編集を参照してください。 –

2

一般的に、私は関数に別のパラメータとして読み取るバイト数を追加することを好むだろう:

(defun read-integer (stream bytes) 
    (check-type bytes (integer 1 *)) 
    (loop :repeat bytes 
     :for b := (read-byte stream) 
     :for n := b :then (+ (* n 256) b) 
     :finally (return n))) 

符号の有無とエンディアンはキーワード引数として追加することができます。このプログラミング方法は、わかりやすいコードに適しており、SLIMEなどのツールを使って簡単にナビゲートすることもできます。

これをマクロで展開することは有効な最適化戦略であり、私はRainer's answerに従います。

ストリームから数値を読み取る特定のケースでは、タイトなループで多く使用される傾向があるため、最適化は最初から有効な目標です。

ただし、これを行う場合は、生成される内容も徹底的に文書化する必要があります。コードの読者がオペレータread8besを見ると、彼はそれがどこで定義されたのかを簡単には知りません。あなたは彼を助ける必要があります。

+1

このような一般的な関数については、8ビットバイトの仮定も文書化する必要があります。;-) –

関連する問題