2017-01-01 17 views
8

Tôi đang học Common Lisp từ Practical Common Lisp. Nó có một ví dụ về các hàm trợ giúp để đọc và ghi các tệp nhị phân trong Chương 24. Dưới đây là một ví dụ:Làm thế nào để viết các chức năng tương tự trong Common Lisp?

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

Tôi có thể viết các chức năng để đọc các loại số nhị phân khác. Nhưng tôi nghĩ rằng làm như vậy vi phạm nguyên tắc DRY. Bên cạnh đó, các hàm này sẽ giống nhau, vì vậy tôi đã cố gắng tạo ra các hàm với các macro.

(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)))))) 

Nó hoạt động. Nó tạo ra các chức năng để đọc và viết các số nguyên chưa ký và được ký trong các kích thước 1, 2, 4 và 8. SLIME hiểu nó. Nhưng tôi tự hỏi nếu có những cách tốt hơn.

Cách tốt nhất để viết một loạt các chức năng tương tự trong Common Lisp là gì?

Trả lời

9

Có một số vấn đề với mã này, mặc dù cách tiếp cận chung để có các hàm tạo macro là tốt.

Naming

Các macro không nên được đặt tên là make-..., bởi vì họ không phải là chức năng mà làm cho một cái gì đó, nhưng macro trong đó xác định một hàm.

thế hệ Mã

EVAL-WHEN ... EVAL thực sự là xấu và không nên được sử dụng theo cách này.

Cách tốt hơn là viết macro mở rộng thành progn với định nghĩa hàm.

Nếu tôi muốn sử dụng EVAL, thì tôi sẽ không cần viết các macro tạo mã, mà chỉ đơn giản là tạo các hàm tạo mã. Nhưng tôi không muốn sử dụng EVAL, tôi muốn tạo mã cho trình biên dịch trực tiếp. Nếu tôi có các macro tạo mã, thì tôi không cần EVAL.

EVAL không phải là một ý tưởng hay vì không rõ mã sẽ được biên dịch - sẽ phụ thuộc vào việc triển khai thực hiện. Ngoài ra việc đánh giá sẽ diễn ra tại thời gian biên dịch và thời gian tải. Nó sẽ là tốt hơn để biên dịch các chức năng tại thời gian biên dịch và chỉ tải chúng tại thời gian tải. Trình biên dịch tệp cũng có thể bỏ lỡ tối ưu hóa có thể cho các hàm được đánh giá.

(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)))) 

Thay vì EVAL-WHEN ... EVAL chúng ta định nghĩa một macro khác và sau đó chúng tôi sử dụng nó sau này:

(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)))))) 

Bây giờ chúng ta có thể sử dụng trên vĩ mô để tạo ra tất cả các chức năng:

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

Bạn có thể xem phần mở rộng tại đây:

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)) 

Mỗi dạng con sau đó sẽ được mở rộng thành các định nghĩa hàm.

Bằng cách này trình biên dịch chạy các macro để tạo tất cả mã tại thời gian biên dịch và sau đó trình biên dịch có thể tạo mã cho tất cả các hàm.

Efficiency/Defaults

Trong một chức năng cấp thấp nhất tôi có thể không muốn sử dụng một tham số &optional. Cuộc gọi mặc định sẽ nhận giá trị từ liên kết động và, tệ hơn, *standard-input*/*standard-output* có thể không phải là luồng mà READ-BYTE hoặc WRITE-BYTE hoạt động. Không phải trong mọi triển khai, bạn có thể sử dụng luồng đầu vào/đầu ra tiêu chuẩn làm luồng nhị phân.

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. 

tôi cũng có thể muốn khai báo tất cả các chức năng được tạo ra để được inlined.

Tuyên bố loại sẽ là một điều khác cần suy nghĩ.

Summmary: không sử dụng EVAL.

+0

Tại sao không sử dụng '& tùy chọn'? Nếu nó cho hiệu quả, nó vẫn áp dụng nếu các chức năng được inlined? – nisekgao

+0

@nisekgao: xem chỉnh sửa của tôi. –

2

Nói chung, tôi muốn chỉ cần thêm số byte để đọc như một tham số cho hàm:

(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))) 

signedness và endianness có thể được thêm vào như các đối số từ khóa. Cách lập trình này là tốt cho mã dễ hiểu cũng dễ dàng điều hướng thông qua các công cụ như SLIME.

Việc đăng ký thông qua macro này là chiến lược tối ưu hóa hợp lệ và tôi trì hoãn Rainer's answer.

Trong trường hợp cụ thể của số đọc từ luồng, tối ưu hóa có thể là mục tiêu hợp lệ ngay từ đầu, vì điều này có xu hướng sử dụng rất nhiều trong các vòng chặt chẽ.

Nếu bạn làm điều này, tuy nhiên, bạn cũng nên ghi chép kỹ những gì được tạo. Nếu một người đọc mã nhìn thấy một nhà điều hành read8bes, anh ta không thể dễ dàng tìm ra nơi nó được xác định. Bạn cần giúp anh ta.

+1

Đối với một chức năng chung như vậy, bạn cũng sẽ cần phải tài liệu giả định của 8 bit byte ... ;-) –

Các vấn đề liên quan