2017-01-01 68 views
8

我正在學習從Practical Common Lisp的Common Lisp。它具有讀取和第24章寫二進制文件下面是一個例子輔助函數的例子:如何在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-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)))) 

取而代之的是EVAL-WHEN ... 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)) 

你可以請參閱此處的擴展:

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-BYTEWRITE-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. 

我可能還需要聲明所有生成的函數內聯。

類型聲明是另一回事。

Summmary:不使用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位字節的假設...... ;-) –