请教一个(并不)低级的plist问题

在写一个工具的过程中遇到一个plist问题: 我解析了org headline的properties,把它存成一个list(plist),就像这样:

(NAME "hehe" AGE "df")  ; 这样应该是一个地道的plist吧?

当然,它是直接被存入一个变量的,就像这样:

(setq a-plist (get-the-plist))

然后,神奇的事情来了:

(plist-get a-plist 'NAME)      ; --> nil,竟然是nil。。。

我踩到了什么坑了吗?

以下是具体代码:


(require 'org-element)
(require 'json)
(require 'cl)
(require 'dash)

(defun oh/get-ast (file-path)
  (with-temp-buffer
    (insert-file-contents file-path)
    (org-mode)
    (org-element-parse-buffer)))

(defun mistkafka/habitica/get-headline-section-child (child-symbol headline-element)
  "获取HEADLINE-ELEMENT的section的child. 合法的CHILD-SYMBOL有:
`planning' `property-drawer' `plain-list' `plain-list' `garagraph'等"
  
  (let ((section-element (->> (org-element-contents headline-element)
			      (nth 0))))
    (if section-element
	(alist-get child-symbol section-element)
      nil)))


(setq ast (oh/get-ast "./tmp.org"))
(setq a-heading (nth 3 ast))
(setq a-property-drawer (mistkafka/habitica/get-headline-section-child 'property-drawer a-headline))
(setq a-plist (cl-loop for x on a-property-drawer
         for y = (car x)
         for child-name = (car y)
         for child-val  = (nth 1 y)
         when (equal child-name 'node-property)
         collect (make-symbol (plist-get child-val :key))
         when (equal child-name 'node-property)
         collect (plist-get child-val :value)))

a-plist                                           ; --> (HABITICATE-ID "12843" NAME "dfksd" AGE "323" ADDRESS "fsdf, resdf, rs" LAST_REPEAT "[2018-04-12 Thu 23:35]")
(plist-get a-plist 'NAME)               ; --> nil

对应的tmp.org文件的内容是:

#+TITLE: Test

* NEXT learn xxx
  DEADLINE: <2018-04-13 Fri +1d>
  :PROPERTIES:
  :HABITICATE-ID: 12843
  :NAME:     dfksd
  :AGE:      323
  :ADDRESS:  fsdf, resdf, rs
  :LAST_REPEAT: [2018-04-12 Thu 23:35]
  :END:
  - State "DONE"       from "TODO"       [2018-04-12 Thu 23:35]
  - State "TODO"       from "DONE"       [2018-04-12 Thu 23:35]
  - State "DONE"       from "TODO"       [2018-04-12 Thu 23:35]
  need learn
** TODO learn x1
** TODO learn x2
* DONE use hehe [1/2]
  DEADLINE: <2018-04-10 Tue>
  :LOGBOOK:
  - State "DONE"       from "CANCELLED"  [2018-03-28 Wed 21:22] \\
    asdadf
  - State "CANCELLED"  from "DONE"       [2018-03-28 Wed 21:22] \\
    asdfad
  - State "DONE"       from "TODO"       [2018-03-28 Wed 21:22]
  :END:
  jasd
  asdfs
  - [X] asdf
  - [ ] asdfsdf
    - head asdf
    - ad asd

* TODO learn yyy
* TODO learn zzz


你的 a-plist 不是一个 plist 吧?

(let ((a-plist '(NAME "hehe" AGE "df")))
  (plist-get a-plist 'NAME))
  
;; => hehe

这个实验我也做过。这样就可以,那为什么我那个不是呢?

一样是 '(key1 value1 key2 value2) 的结构啊!

这里参数顺序写反了。我也常犯这样的错误。考虑到顺序之不一致,不会记错才怪:

(plist-get plist 'key)
(alist-get 'key alist)
(gethash 'key hash-table)

(nth n list)
(elt seq n)
(aref array idx)

抱歉,整理 片段代码的时候写错了。

(plist-get a-plist 'NAME) ; --> 也是nil

一定是你的 a-plist 有问题,把实际的 a-plist 打印出来,实验结果不等于实际情况。

好思路,我把work的plist打印出来比较一翻。

其实我想看看plist-get这个宏的,但是发现它竟然是c代码实现。

你没找到问题是什么,就先假定了跟 plist-get 有什么关系。

collect (intern (plist-get child-val :key))

2 个赞

解釋一下:

make-symbolintern 的生成的 symbol 对於 plist-get 有区別的原因在于,plist-get (可以理解为)是用 eq 比較 symbol 的,而 make-symbol 生成的 symbol 是不能用 eq 比較的。

(eq 'ADS (make-symbol "ADS"))
nil

(eq (make-symbol "ADS") (make-symbol "ADS"))
nil

(eq 'ADS (intern "ADS"))
t

具體原理涉及 call-by-reference,即指针之类的。简言之 eq 就是比較两個指针。intern 生成的 symbol 是返回指针的,且对于同一個 symbol 返回的是同一個指针,而 make-symbol 生成的 symbol 不返回指针,故而不能用 eq 比較。

参考 Emacs Lisp 的 symbol 实現:https://www.gnu.org/software/emacs/manual/html_node/elisp/Creating-Symbols.html

3 个赞

补充一句, lisp 添加这种特性,一个用途(也可能是主要的)是为了防止编写宏的时候, 出现变量冲突。。。。。

还有一種说法是以前內存很宝貴,用 uninterned symbol 可以节约內存。:joy:

:joy::joy::joy::joy::joy::joy: 其实有时候研究 lisp 历史很有意思, 你会发现现在难以理解的特性的历史背景

之前都没注意到有 make-symbol 这个函数。看了一下它的定义,跟 intern 确实不太一样:

  • intern 首先会根据参数 STRING 从表中查找现存的符号,如果不存在则创建。所以两次 (intern "foo") 返回的是同一个 symbol;而两次 (make-symbol "foo") 返回的是不同的 symbol,所以 eq 比较结果为 nil。
  • intern 返回的 symbol,有对应的 value;而 make-symbol 返回的 symbol,其对应的 value 是 void

源代码可以看出两个函数到底干了啥:

 Elisp           | C
-----------------|----------------------------------------
 (intern STRING) | ...
                 | tem = oblookup(obarray, ...STRING, ...);
                 | ...
                 | return tem;
 Elisp              | C
--------------------|----------------------------------------
 (make-symbol NAME) | ...
                    | init_symbol (val, name); 
                    |    .--^-------------------------------------.
                    |    | struct Lisp_Symbol *p = XSYMBOL (val); |
                    |    | ...                                    |
                    |    | set_symbol_name (val, name);           |
                    |    | ...                                    |
                    |    | SET_SYMBOL_VAL (p, Qunbound);          |
                    |    '----------------------------------------'
                    | ... 
                    | return val;
2 个赞

@tumashu @LdBeth @twlz0ne 非常感谢各位的详细解释~ 被坑得深刻。话说,我貌似曾经在哪里看过这两个区别(一篇写Symbol的文章),但是那时候对lisp只是走马观花。上次做东西,只想着 string -> symbol,所以从C-h f里搜索到的就是make-symbol :joy:

但是一直不work,我就改用hash绕过了,被坑了之后,现在几乎一直都在用hash。

顺便关联一下:

make-symbol跟 es6的Symbol一样 Symbol('hehe') !== Symbol('hehe') --> true

答案就按顺序取第一位回答到点子上的 @tumashu 吧。

感觉该去弄一份emacs的c源码跟 emacs的doc关联起来,这样遇到c实现的函数,可以打开“轻查”一翻。

hash table 好啊,其实 plist 这种和列表相关不应该被鼓励当作数据结构使用的,效率比较低,然后有意外被副作用修改的风险。

直接 M-. 跳进去就能看doc和源码了,或者推荐 helpful 包

你再看看10楼和14楼呢,你这里的make-symbol改成intern才对。


这些你贴的文档里都没讲,按文档说的,这样解释就够了?:

intern去obarray找,找不到就创建,而make-symbol是独立于obarray创建的uninterned symbol

我讲的是概念,文档里的是具体实现,是相互补充的。是否返回指针(引用)的区别就是由是否从 obarray 返回值实现的,