2012-08-07 101 views
2

有人可以幫我理解push可以作爲一個宏來實現嗎?下面的幼稚版本評估地方形式兩次,評估元素形式之前這樣做:我怎樣才能實現推宏?

(defmacro my-push (element place) 
    `(setf ,place (cons ,element ,place))) 

但是,如果我嘗試如下解決這個問題,然後我setf -ing錯了地方:

(defmacro my-push (element place) 
    (let ((el-sym (gensym)) 
     (place-sym (gensym))) 
    `(let ((,el-sym ,element) 
      (,place-sym ,place)) 
     (setf ,place-sym (cons ,el-sym ,place-sym))))) 

CL-USER> (defparameter *list* '(0 1 2 3)) 
*LIST* 
CL-USER> (my-push 'hi *list*) 
(HI 0 1 2 3) 
CL-USER> *list* 
(0 1 2 3) 

我怎麼能setf正確的地方沒有評估兩次?

+0

回頭看看SBCL源代碼,我發現他們使用一次性宏來處理rplaca(請參閱此鏈接瞭解更多信息http://stackoverflow.com/questions/9808928/understanding-how-to-implement-once -only-lisp-macro)。我會調查rplaca進一步瞭解。 – 2012-08-07 22:43:08

+0

@PaulNathan您能否提供您正在查看的SBCL版本和源位置?因爲我在'src/code/early-setf.lisp'(SBCL 1.0.58)中看到'(defmacro-mundanely push(obj place&environment env)...',它沒有使用'once-only'。 – 2012-08-08 07:27:46

回答

3

這樣做的權利似乎是一個更復雜些。例如,在SBCL 1.0.58爲push代碼:

(defmacro-mundanely push (obj place &environment env) 
    #!+sb-doc 
    "Takes an object and a location holding a list. Conses the object onto 
    the list, returning the modified list. OBJ is evaluated before PLACE." 
    (multiple-value-bind (dummies vals newval setter getter) 
     (sb!xc:get-setf-expansion place env) 
    (let ((g (gensym))) 
     `(let* ((,g ,obj) 
       ,@(mapcar #'list dummies vals) 
       (,(car newval) (cons ,g ,getter)) 
       ,@(cdr newval)) 
     ,setter)))) 

所以閱讀文檔上get-setf-expansion似乎是有用的。

根據記錄,生成的代碼看起來相當不錯:

推到一個符號:

(push 1 symbol) 

擴展到

(LET* ((#:G906 1) (#:NEW905 (CONS #:G906 SYMBOL))) 
    (SETQ SYMBOL #:NEW905)) 

推入SETF-能功能(假設symbol指向列表清單):

(push 1 (first symbol)) 

擴展到

(LET* ((#:G909 1) 
     (#:SYMBOL908 SYMBOL) 
     (#:NEW907 (CONS #:G909 (FIRST #:SYMBOL908)))) 
    (SB-KERNEL:%RPLACA #:SYMBOL908 #:NEW907)) 

所以,除非你需要一些時間來研究setfsetf expansions和公司,這看起來相當神祕的(它可能仍然即使他們研究後看起來如此)。 OnLisp中的'通用變量'一章也許很有用。

提示:如果您編譯自己的SBCL(不那麼難),請將--fancy參數傳遞給make.sh。這樣您就可以快速查看SBCL中函數/宏的定義(例如,在Emacs + SLIME中使用M-。)。顯然,不要刪除這些源代碼(你可以在install.sh之後運行clean.sh,以節省90%的空間)。

1

考慮看看如何在現有的(在SBCL,至少)做的事情,我看到:

* (macroexpand-1 '(push 1 *foo*)) 

(LET* ((#:G823 1) (#:NEW822 (CONS #:G823 *FOO*))) 
    (SETQ *FOO* #:NEW822)) 
T 

所以,我想,在你的版本相結合,這會產生,一個什麼混合可能會做:

(defmacro my-push (element place) 
    (let ((el-sym (gensym)) 
     (new-sym (gensym "NEW"))) 
    `(let* ((,el-sym ,element) 
      (,new-sym (cons ,el-sym ,place))) 
     (setq ,place ,new-sym))))) 

幾個意見:

  1. 這似乎與合作無論是setqsetf。根據你實際想要解決什麼問題(我認爲重寫push不是實際的最終目標),你可能會喜歡其中一個。

  2. 請注意place仍然會得到兩次評估......儘管它至少在評估element後才這樣做。雙重評估是你實際需要避免的嗎? (鑑於內置的push沒有,我仍然想知道你是否能夠... ...,儘管我在花費大量時間思考它之前寫了這些。)鑑於它是某種東西那需要評估爲「place」,也許這是正常的?

  3. 使用let*代替let使我們能夠在,new-sym設置使用,el-sym。這發生在cons發生的位置,因此它在,place的第一次評估中以及,element的評估後評估。在評估順序方面,這可能會讓你得到你需要的東西嗎?

  4. 我認爲你的第二個版本最大的問題是,你的setf確實需要對傳入的符號進行操作,而不是對gensym符號進行操作。

希望這有助於...(我還是有點新的這一切我自己,所以我在這裏做一些猜測。)

+1

有趣的是,HyperSpec認爲地點表單只會被評估一次,但它看起來像SBCL知道從一個符號獲取兩次的值是無害的,對於一個可能產生副作用的地方表單來說:CL-USER>(macroexpand-1 '(推4(my-list-func))) (LET *((##:G31 4)(#:G30(CONS#:G31(MY-LIST-FUNC)))) #:G30 (FUNCALL #'(SETF MY-LIST-FUNC)#:G30))。SETF的這種形式是否避免了第二次評估? – jjoelson 2012-08-09 13:28:02

+1

當我實際運行上述時,我得到「未定義函數(SETF MY-LIST-FUNC)」。像雙重評估問題可以通過不允許自己設置的地方來避免。 – jjoelson 2012-08-09 13:34:18