zet*_*exe 4 lisp common-lisp gensym
上下文化:我一直在做一个大学项目,我必须为正则表达式编写一个解析器并构建相应的 epsilon-NFA。我必须在 Prolog 和 Lisp 中做到这一点。我不知道这样的问题是否允许,如果不允许,我道歉。
我听到了我的一些同学谈论他们如何使用该函数gensym,我问他们有什么它做,甚至检查了在线,但我真的不明白这是什么功能呢既不为什么或者是最好的时候使用它。
特别是,我更感兴趣的是它在 Lisp 中的作用。谢谢你们。
Rai*_*wig 12
GENSYM创建独特的符号。每次调用都会创建一个新符号。该符号通常有一个名称,其中包含一个数字,该数字是向上计数的。该名称也是唯一的(符号本身已经是唯一的)并带有数字,以便人类读者可以识别源代码中不同的未插入符号。
CL-USER 39 > (gensym)
#:G1083
CL-USER 40 > (gensym)
#:G1084
CL-USER 41 > (gensym)
#:G1085
CL-USER 42 > (gensym)
#:G1086
Run Code Online (Sandbox Code Playgroud)
gensym 经常在 Lisp 宏中用于代码生成,当宏需要创建新标识符时,这些标识符不会与现有标识符发生冲突。
示例:我们要将 Lisp 表单的结果加倍,并且我们确保 Lisp 表单本身只计算一次。我们通过将值保存在局部变量中来做到这一点。局部变量的标识符将由 计算gensym。
CL-USER 43 > (defmacro double-it (it)
(let ((new-identifier (gensym)))
`(let ((,new-identifier ,it))
(+ ,new-identifier ,new-identifier))))
DOUBLE-IT
CL-USER 44 > (macroexpand-1 '(double-it (cos 1.4)))
(LET ((#:G1091 (COS 1.4)))
(+ #:G1091 #:G1091))
T
CL-USER 45 > (double-it (cos 1.4))
0.33993432
Run Code Online (Sandbox Code Playgroud)
对现有答案的一些澄清(因为操作员还不知道典型的常见 lisp 宏工作流程):
考虑一下double-it由先生提出的宏。乔斯维格。为什么我们要费心去创造这一大堆let?当它可以简单地:
(defmacro double-it (it)
`(+ ,it ,it))
Run Code Online (Sandbox Code Playgroud)
好的,它似乎有效:
CL-USER> (double-it 1)
;;=> 2
Run Code Online (Sandbox Code Playgroud)
但是看看这个,我们想要增加x并加倍它
CL-USER> (let ((x 1))
(double-it (incf x)))
;;=> 5
;; WHAT? it should be 4!
Run Code Online (Sandbox Code Playgroud)
原因可以从宏展开中看出:
(let ((x 1))
(+ (setq x (+ 1 x)) (setq x (+ 1 x))))
Run Code Online (Sandbox Code Playgroud)
你看,由于宏不计算表单,只是将它拼接成生成的代码,它导致incf被执行两次。
简单的解决方案是将它绑定到某个地方,然后将结果加倍:
(defmacro double-it (it)
`(let ((x ,it))
(+ x x)))
CL-USER> (let ((x 1))
(double-it (incf x)))
;;=> 4
;; NICE!
Run Code Online (Sandbox Code Playgroud)
现在好像没问题了。它真的像这样扩展:
(let ((x 1))
(let ((x (setq x (+ 1 x))))
(+ x x)))
Run Code Online (Sandbox Code Playgroud)
好的,那gensym东西呢?
假设您想打印一些消息,然后将您的值加倍:
(defmacro double-it (it)
`(let* ((v "DOUBLING IT")
(val ,it))
(princ v)
(+ val val)))
CL-USER> (let ((x 1))
(double-it (incf x)))
;;=> DOUBLING IT
;;=> 4
;; still ok!
Run Code Online (Sandbox Code Playgroud)
但是如果你不小心命名了 valuev而不是x:
CL-USER> (let ((v 1))
(double-it (incf v)))
;;Value of V in (+ 1 V) is "DOUBLING IT", not a NUMBER.
;; [Condition of type SIMPLE-TYPE-ERROR]
Run Code Online (Sandbox Code Playgroud)
它抛出这个奇怪的错误!看看扩展:
(let ((v 1))
(let* ((v "DOUBLING IT") (val (setq v (+ 1 v))))
(princ v)
(+ val val)))
Run Code Online (Sandbox Code Playgroud)
它v用字符串从外部作用域遮蔽 ,当您尝试添加 1 时,显然不能。太糟糕了。
另一个例子,假设你想调用该函数两次,并以列表形式返回 2 个结果:
(defmacro two-funcalls (f v)
`(let ((x ,f))
(list (funcall x ,v) (funcall x ,v))))
CL-USER> (let ((y 10))
(two-funcalls (lambda (z) z) y))
;;=> (10 10)
;; OK
CL-USER> (let ((x 10))
(two-funcalls (lambda (z) z) x))
;; (#<FUNCTION (LAMBDA (Z)) {52D2D4AB}> #<FUNCTION (LAMBDA (Z)) {52D2D4AB}>)
;; NOT OK!
Run Code Online (Sandbox Code Playgroud)
这类错误非常讨厌,因为您不能轻易说出发生了什么。解决办法是什么?显然不要v在宏中命名值。您需要生成一些没有人会在其代码中重现的复杂名称,例如my-super-unique-value-identifier-2019-12-27. 这可能会救你,但你仍然不能确定。这就是为什么 gensym 在那里:
(defmacro two-funcalls (f v)
(let ((fname (gensym)))
`(let ((,fname ,f))
(list (funcall ,fname ,v) (funcall ,fname ,v)))))
Run Code Online (Sandbox Code Playgroud)
扩展到:
(let ((y 10))
(let ((#:g654 (lambda (z) z)))
(list (funcall #:g654 y) (funcall #:g654 y))))
Run Code Online (Sandbox Code Playgroud)
您只需为生成的代码生成 var 名称,它保证是唯一的(意味着没有两个gensym调用会为运行时会话生成相同的名称),
(loop repeat 3 collect (gensym))
;;=> (#:G645 #:G646 #:G647)
Run Code Online (Sandbox Code Playgroud)
它仍然可能以某种方式与用户 var 发生冲突,但每个人都知道命名并且不会调用 var #:GXXXX,因此您可以认为这是不可能的。您可以进一步保护它,添加前缀
(loop repeat 3 collect (gensym "MY_GUID"))
;;=> (#:MY_GUID651 #:MY_GUID652 #:MY_GUID653)
Run Code Online (Sandbox Code Playgroud)