lexical binding下scope失效?

(setq list-of-list '(((value . (lambda () '("1" "2"))) (prop . 0.2) (candidate . t))
                     ((value . (lambda () '("1" "3"))) (prop . 0.4) (face . ((t (:foreground "red")))))
                     ((value . (lambda () '("2" "4"))) (prop . 0.4) (face . ((t (:foreground "green")))))))

(defun fun (list-of-list)
    (dolist (alist list-of-list)
      (let ((func (alist-get 'value alist)))
        (setf (alist-get 'value alist) (funcall func)))))

(fun list-of-list)

list-of-list
; => (((value "1" "2") (prop . 0.2) (candidate . t)) ((value "1" "3") (prop . 0.4) (face (t ...))) ((value "2" "4") (prop . 0.4) (face (t ...))))

发现list-of-list被修改了,从(lambda () '("1" "2")))变成了("1" "2")


而另一个实验中,list-of-list没有被改变(期望行为)

(setq s1 "s"
      s2 "s"
      s3 "s")

(setq list-of-list '(((value . s1) s2 s3) ((value . s3) s2 s1)))

(defun fun (list-of-list)
  (dolist (list list-of-list)
    (fset (alist-get 'value list) '(value . "ss"))))

(fun list-of-list)

list-of-list
; => (((value . s1) s2 s3) ((value . s3) s2 s1))

这是为什么啊?

害得我折腾两个小时 :face_with_symbols_over_mouth:

副作用?

(setq s1 "s1"
      s2 "s2"
      s3 "s3")

(setq foo (list s1 s2 s3))

(setf (cadr foo) 2)
foo ;; => ("s1" 2 "s3")

(defun test (list)
  (setf (cadr list) 20))
(test foo)
foo ;; => ("s1" 20 "s3")

同理 setcarsetcdrnconc危害大


第二个实验

(mapcar (lambda (x)
          (symbol-function (alist-get 'value x)))
        list-of-list)
;; => ((value . "ss") (value . "ss"))

你都用了 setf 为什么会改变心理就没点数?

还是说你都不知道 setf 和 fset 的作用是完全两回事?

哦,你以为 lexical binding 作用是 copy value 吧?果然是被 Python 害的。Copy by reference 了解一下。

这和 lexical scoping 一点关系都没有,还不是你乱取参数名把自己玩糊涂了,就算用

(defun fun (abc)
  (dolist (list abc)
    (fset (alist-get 'value list) '(value . "ss"))))

结果也是一样的。

不清楚你的疑问在哪里,或许你没注意 setffset 的区别?

(let ((pair '(k . v)))
  (setf (cdr pair) 100)
  pair)
;; => (k . 100)

(let ((pair '(k . v)))
  (fset (cdr pair) (lambda () 100))
  (cons pair (funcall (cdr pair))))
;; => ((k . v) . 100)

你大概是混淆了这两个函数的作用,不关 lexical binding 的事。

setf 应该跟 setq 对比

  • (setq SYM VAL SYM VAL ...) 修改符号对应的值
  • (setf PLACE VAL PLACE VAL ...) 修改位置对应的值

关键就在于 PLACE,它不是符号:

(let ((al '((foo . 1)
            (bar . 2))))
  (setf (car (assoc 'foo al)) 'quux) ;; 修改 key
  al)
;; => ((quux . 1) (bar . 2))

alist 中的这些 “key” 不能算做符号,只是数据而已。换一种写法可能更清晰:

(let ((al '(("foo" . 1)
            ("bar" . 2))))
  (setf (car (assoc "foo" al)) "quux") ;; 修改 key
  al)
;; => (("quux" . 1) ("bar" . 2))

fset 则…仍然可以跟 setq 对比

  • (setq SYM VAL SYM VAL ...) 修改符号对应的值
  • (fset SYM DEFINITON) 修改符号对应的定义

虽然参数都是 SYM,但实际是不同的东西:

(defun foo () (princ "FOO\n"))

(foo)
;; => FOO

;; 创建了一个新的符号:变量 foo
(setq foo
      (lambda () (princ "BAR\n")))

;; 所以函数行为不变
(foo)
;; => FOO

defun 出来的符号,用 fset 才能修改:

(defun foo () (princ "FOO\n"))

(foo)
;; => FOO

;; 修改的是原来的符号 foo 的定义
(fset 'foo
      (lambda () (princ "BAR\n")))

;; 所以函数行为改变
(foo)
;; => BAR

setf 实现 fset 的效果:

(defun foo () (princ "FOO\n"))

(foo)
;; => FOO

-- ;; 修改的是原来的符号 foo 的定义
-- (fset 'foo
++ ;; 替换符号 foo 定义的值
++ (setf (symbol-function 'foo)
      (lambda () (princ "BAR\n")))

;; 结果改变
(foo)
;; => BAR

虽然 setf 也能达到相同效果,但思路是不一样的。

打个不恰当的比喻,就像是 c 语言里修改指针内容:

#+BEGIN_SRC C :results output :includes <stdio.h> :flags -std=gnu99 -Wall -Werror
char s1[10] = {'f', 'o', 'o'};
char s2[10] = {'b', 'a', 'r'};
char *p = s1;

printf("%s\n", p);

// fset -- 指向新的内容
p = s2;
printf("%s\n", p);

// setf -- 原地修改内容
sprintf(p, "qux");
printf("%s\n", p);
#+END_SRC

#+RESULTS:
: foo
: bar
: qux
2 个赞

谢谢大家,我还有很多要学习啊。打算近期看看书补充一下基础知识。

学习了,已收藏。一点小建议,第二段 snippet 第一次直接调用,第二次用 funcall,容易让人以为 fset 的函数和普通定义的函数调用方式不同。我刚才就产生这样的疑惑,然后自己运行一下才发现其实没有什么不同,(foo) 这样调用也可以。

另外,以我的理解 cl-letf 的用法和 setf 是类似的吧? fletfset 类似?

改了。

本来都是 (funcall foo),从 org 粘贴过来的时候临时改成 (foo),然后没改完。