我喜欢宏。
这里是从 LDAP 中为人们填充属性的代码。我只是碰巧有那个代码,并认为它对其他人有用。
有些人对所谓的宏运行时惩罚感到困惑,所以我在最后添加了一个澄清问题的尝试。
一开始就有重复
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal)))
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(let ((mail (car (ldap:attr-value ent 'mail)))
(uid (car (ldap:attr-value ent 'uid)))
(name (car (ldap:attr-value ent 'cn)))
(phonenumber (car (ldap:attr-value ent 'telephonenumber))))
(setf (gethash uid people)
(list mail name phonenumber))))
people))
您可以将“let 绑定”视为一个局部变量,它在 LET 形式之外消失。注意绑定的形式——它们非常相似,不同之处仅在于 LDAP 实体的属性和将值绑定到的名称(“局部变量”)。有用,但有点冗长,并且包含重复。
追求美丽
现在,如果我们不必进行所有重复,那不是很好吗?一个常见的习惯用法是 WITH-... 宏,它基于一个表达式绑定值,您可以从中获取值。让我们引入我们自己的宏,WITH-LDAP-ATTRS,并在我们的原始代码中替换它。
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal))) ; equal so strings compare equal!
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(with-ldap-attrs (mail uid name phonenumber) ent
(setf (gethash uid people)
(list mail name phonenumber))))
people))
你有没有看到一堆行突然消失了,只剩下一行?这个怎么做?当然是使用宏——编写代码的代码! Lisp 中的宏与您在 C/C++ 中通过使用预处理器找到的宏完全不同:在这里,您可以运行 real Lisp 代码(不是 #define fluff in cpp) 在编译其他代码之前生成 Lisp 代码。宏可以使用任何真正的 Lisp 代码,即普通函数。基本上没有限制。
摆脱丑陋
那么,让我们看看这是如何做到的。为了替换一个属性,我们定义了一个函数。
(defun ldap-attr (entity attr)
`(,attr (car (ldap:attr-value ,entity ',attr))))
反引号语法看起来有点毛茸茸,但它的作用很简单。当您调用 LDAP-ATTRS 时,它会吐出一个列表,其中包含 attr(即逗号)的 值,然后是 car(“列表中的第一个元素”(缺点对,实际上),实际上还有一个函数叫做first,你也可以使用),它接收ldap:attr-value返回的列表中的第一个值。因为这不是我们在编译代码时想要运行的代码(获取属性值是我们在运行程序时想要做的),所以我们不在调用前添加逗号.
无论如何。继续,到宏的其余部分。
(defmacro with-ldap-attrs (attrs ent &rest body)
`(let ,(loop for attr in attrs
collecting `,(ldap-attr ent attr))
,@body))
,@-syntax 是将列表的内容放在某处,而不是实际的列表。
结果
您可以轻松验证这是否会给您带来正确的结果。宏通常是这样写的:你从你想要简化的代码(输出)开始,你想要写的东西(输入),然后你开始塑造宏,直到你的输入给出正确的输出。 macroexpand-1 函数会告诉你你的宏是否正确:
(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
(format t "~a with ~a" mail phonenumber)))
评估为
(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
(phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
(format t "~a with ~a" mail phonenumber))
如果你将扩展宏的 LET-bindings 与开头的代码进行比较,你会发现它的形式是一样的!
编译时与运行时:宏与函数
宏是在编译时运行的代码,具有额外的变化,他们可以随意调用任何普通函数或宏!它只不过是一个花哨的过滤器,接受一些参数,应用一些转换,然后将生成的 s-exp 提供给编译器。
基本上,它可以让您以可以在问题域中找到的动词编写代码,而不是语言中的低级原语!作为一个愚蠢的例子,考虑以下(如果when 还不是内置的)::
(defmacro my-when (test &rest body)
`(if ,test
(progn ,@body)))
if 是一个内置原语,它只会让你在分支中执行 一个 表单,如果你想拥有多个,那么你需要使用progn ::
;; one form
(if (numberp 1)
(print "yay, a number"))
;; two forms
(if (numberp 1)
(progn
(assert-world-is-sane t)
(print "phew!"))))
对于我们的新朋友my-when,我们可以a) 如果我们没有错误分支,则使用更合适的动词,b) 添加隐式排序运算符,即progn::
(my-when (numberp 1)
(assert-world-is-sane t)
(print "phew!"))
不过,编译后的代码永远不会包含my-when,因为在第一遍中,所有宏都被展开,因此不涉及运行时损失!
Lisp> (macroexpand-1 '(my-when (numberp 1)
(print "yay!")))
(if (numberp 1)
(progn (print "yay!")))
注意macroexpand-1 只做一级扩展;扩张有可能(实际上很可能!)继续进一步向下。但是,最终您会遇到通常不太有趣的特定于编译器的实现细节。但是继续扩展结果最终会得到更多细节,或者只是你输入的 s-exp 回来。
希望能澄清事情。宏是一个强大的工具,也是我喜欢的 Lisp 中的一个特性。