有人试图向我推销 Lisp,作为一种超级强大的语言,它可以做任何事情,然后是一些。
是否有Lisp 强大功能的实际代码示例?
(最好与以常规语言编码的等效逻辑一起使用。)
有人试图向我推销 Lisp,作为一种超级强大的语言,它可以做任何事情,然后是一些。
是否有Lisp 强大功能的实际代码示例?
(最好与以常规语言编码的等效逻辑一起使用。)
I like macros.
Here's code to stuff away attributes for people from LDAP. I just happened to have that code lying around and fiigured it'd be useful for others.
Some people are confused over a supposed runtime penalty of macros, so I've added an attempt at clarifying things at the end.
(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))
You can think of a "let binding" as a local variable, that disappears outside the LET form. Notice the form of the bindings -- they are very similar, differing only in the attribute of the LDAP entity and the name ("local variable") to bind the value to. Useful, but a bit verbose and contains duplication.
Now, wouldn't it be nice if we didn't have to have all that duplication? A common idiom is is WITH-... macros, that binds values based on an expression that you can grab the values from. Let's introduce our own macro that works like that, WITH-LDAP-ATTRS, and replace it in our original code.
(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))
Did you see how a bunch of lines suddenly disappeared, and was replaced with just one single line? How to do this? Using macros, of course -- code that writes code! Macros in Lisp is a totally different animal than the ones you can find in C/C++ through the use of the pre-processor: here, you can run real Lisp code (not the #define
fluff in cpp) that generates Lisp code, before the other code is compiled. Macros can use any real Lisp code, i.e., ordinary functions. Essentially no limits.
So, let's see how this was done. To replace one attribute, we define a function.
(defun ldap-attr (entity attr)
`(,attr (car (ldap:attr-value ,entity ',attr))))
The backquote syntax looks a bit hairy, but what it does is easy. When you call LDAP-ATTRS, it'll spit out a list that contains the value of attr
(that's the comma), followed by car
("first element in the list" (cons pair, actually), and there is in fact a function called first
you can use, too), which receives the first value in the list returned by ldap:attr-value
. Because this isn't code we want to run when we compile the code (getting the attribute values is what we want to do when we run the program), we don't add a comma before the call.
Anyway. Moving along, to the rest of the macro.
(defmacro with-ldap-attrs (attrs ent &rest body)
`(let ,(loop for attr in attrs
collecting `,(ldap-attr ent attr))
,@body))
The ,@
-syntax is to put the contents of a list somewhere, instead of the actual list.
You can easily verify that this will give you the right thing. Macros are often written this way: you start off with code you want to make simpler (the output), what you want to write instead (the input), and then you start molding the macro until your input gives the correct output. The function macroexpand-1
will tell you if your macro is correct:
(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
(format t "~a with ~a" mail phonenumber)))
evaluates to
(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
(phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
(format t "~a with ~a" mail phonenumber))
If you compare the LET-bindings of the expanded macro with the code in the beginning, you'll find that it is in the same form!
A macro is code that is run at compile-time, with the added twist that they can call any ordinary function or macro as they please! It's not much more than a fancy filter, taking some arguments, applying some transformations and then feeding the compiler the resulting s-exps.
Basically, it lets you write your code in verbs that can be found in the problem domain, instead of low-level primitives from the language! As a silly example, consider the following (if when
wasn't already a built-in)::
(defmacro my-when (test &rest body)
`(if ,test
(progn ,@body)))
if
is a built-in primitive that will only let you execute one form in the branches, and if you want to have more than one, well, you need to use progn
::
;; one form
(if (numberp 1)
(print "yay, a number"))
;; two forms
(if (numberp 1)
(progn
(assert-world-is-sane t)
(print "phew!"))))
With our new friend, my-when
, we could both a) use the more appropriate verb if we don't have a false branch, and b) add an implicit sequencing operator, i.e. progn
::
(my-when (numberp 1)
(assert-world-is-sane t)
(print "phew!"))
The compiled code will never contain my-when
, though, because in the first pass, all macros are expanded so there is no runtime penalty involved!
Lisp> (macroexpand-1 '(my-when (numberp 1)
(print "yay!")))
(if (numberp 1)
(progn (print "yay!")))
Note that macroexpand-1
only does one level of expansions; it's possible (most likely, in fact!) that the expansion continues further down. However, eventually you'll hit the compiler-specific implementation details which are often not very interesting. But continuing expanding the result will eventually either get you more details, or just your input s-exp back.
Hope that clarifies things. Macros is a powerful tool, and one of the features in Lisp I like.
我能想到的最好的例子是 Paul Graham 的书On Lisp。可以从我刚刚提供的链接下载完整的 PDF。您还可以尝试Practical Common Lisp(也可以在网络上完全获得)。
我有很多不切实际的例子。我曾经用大约 40 行 lisp 编写了一个程序,它可以解析自己,将其源视为 lisp 列表,对列表进行树遍历并构建一个表达式,如果 waldo 标识符存在于源中或计算为如果 waldo 不在场,则为零。返回的表达式是通过将对 car/cdr 的调用添加到已解析的原始源来构造的。我不知道如何用 40 行代码用其他语言做到这一点。也许 perl 可以用更少的代码来完成。
您可能会发现这篇文章很有帮助: http: //www.defmacro.org/ramblings/lisp.html
也就是说,很难给出关于 Lisp 强大功能的简短实用示例,因为它真的只在不平凡的代码中闪耀。当您的项目发展到一定规模时,您将欣赏 Lisp 的抽象工具并为您一直在使用它们而感到高兴。另一方面,相当短的代码示例永远不会让您满意地展示 Lisp 的出色之处,因为其他语言的预定义缩写在小示例中看起来比 Lisp 在管理特定领域抽象方面的灵活性更具吸引力。
Lisp 中有很多杀手级功能,但宏是我特别喜欢的功能,因为在语言定义的内容和我定义的内容之间不再存在真正的障碍。例如,Common Lisp 没有while构造。我曾经在我的脑海中实现它,同时走路。它简单明了:
(defmacro while (condition &body body)
`(if ,condition
(progn
,@body
(do nil ((not ,condition))
,@body))))
瞧!您刚刚使用新的基本结构扩展了 Common Lisp 语言。您现在可以执行以下操作:
(let ((foo 5))
(while (not (zerop (decf foo)))
(format t "still not zero: ~a~%" foo)))
哪个会打印:
still not zero: 4
still not zero: 3
still not zero: 2
still not zero: 1
在任何非 Lisp 语言中这样做都留给读者作为练习......
我喜欢Common Lisp 对象系统(CLOS) 和多方法。
大多数(如果不是全部)面向对象的编程语言都具有类和方法的基本概念。Python中的以下代码段定义了 PeelingTool 和蔬菜类(类似于访问者模式):
class PeelingTool:
"""I'm used to peel things. Mostly fruit, but anything peelable goes."""
def peel(self, veggie):
veggie.get_peeled(self)
class Veggie:
"""I'm a defenseless Veggie. I obey the get_peeled protocol
used by the PeelingTool"""
def get_peeled(self, tool):
pass
class FingerTool(PeelingTool):
...
class KnifeTool(PeelingTool):
...
class Banana(Veggie):
def get_peeled(self, tool):
if type(tool) == FingerTool:
self.hold_and_peel(tool)
elif type(tool) == KnifeTool:
self.cut_in_half(tool)
您将peel
方法放入 PeelingTool 并让 Banana 接受它。但是,它必须属于 PeelingTool 类,因此只有在您拥有 PeelingTool 类的实例时才能使用它。
Common Lisp 对象系统版本:
(defclass peeling-tool () ())
(defclass knife-tool (peeling-tool) ())
(defclass finger-tool (peeling-tool) ())
(defclass veggie () ())
(defclass banana (veggie) ())
(defgeneric peel (veggie tool)
(:documentation "I peel veggies, or actually anything that wants to be peeled"))
;; It might be possible to peel any object using any tool,
;; but I have no idea how. Left as an exercise for the reader
(defmethod peel (veggie tool)
...)
;; Bananas are easy to peel with our fingers!
(defmethod peel ((veggie banana) (tool finger-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(peel-with-fingers right-hand tool banana)))
;; Slightly different using a knife
(defmethod peel ((veggie banana) (tool knife-tool))
(with-hands (left-hand right-hand) *me*
(hold-object left-hand banana)
(cut-in-half tool banana)))
任何东西都可以用图灵完备的任何语言编写;语言之间的区别在于您必须跳过多少圈才能获得等效的结果。
像Common Lisp这样功能强大的语言,具有宏和 CLOS 等功能,可以让您快速轻松地获得结果,而无需跳过太多的障碍,以至于您要么满足于低于标准的解决方案,要么发现自己变成了一只袋鼠。
实际上,一个很好的实际例子是 Lisp LOOP 宏。
http://www.ai.sri.com/pkarp/loop.html
LOOP 宏就是——一个 Lisp 宏。然而,它基本上定义了一个迷你循环 DSL(领域特定语言)。
当您浏览那个小教程时,您会发现(即使是新手)很难知道代码的哪一部分是 Loop 宏的一部分,哪一部分是“正常”的 Lisp。
这是 Lisps 表现力的关键组成部分之一,新代码确实无法与系统区分开来。
例如,在 Java 中,您可能无法(一目了然)知道程序的哪一部分来自标准 Java 库而不是您自己的代码,甚至是第三方库,但您确实知道代码的哪一部分是 Java 语言,而不是简单地对类进行方法调用。诚然,这都是“Java 语言”,但作为程序员,您仅限于将您的应用程序表达为类和方法的组合(现在是注释)。而在 Lisp 中,从字面上看,一切都可以争夺。
考虑将 Common Lisp 连接到 SQL 的 Common SQL 接口。在这里,http://clsql.b9.com/manual/loop-tuples.html,它们展示了如何扩展 CL 循环宏以使 SQL 绑定成为“一等公民”。
您还可以观察诸如“[select [first-name] [last-name] :from [employee] :order-by [last-name]]”之类的结构。这是 CL-SQL 包的一部分,并作为“阅读器宏”实现。
看,在 Lisp 中,你不仅可以创建宏来创建新的结构,如数据结构、控制结构等。你甚至可以通过阅读器宏来更改语言的语法。在这里,他们使用读取器宏(在本例中为“[”符号)进入 SQL 模式,以使 SQL 像嵌入式 SQL 一样工作,而不是像许多其他语言那样仅作为原始字符串。
作为应用程序开发人员,我们的任务是将我们的流程和构造转换为处理器可以理解的形式。这意味着我们不可避免地不得不与计算机语言“交谈”,因为它“不理解”我们。
Common Lisp 是少数几个不仅可以自上而下构建应用程序的环境之一,而且我们可以提升语言和环境以适应我们的中途。我们可以在两端编码。
请注意,尽管如此优雅,但它不是灵丹妙药。显然还有其他因素会影响语言和环境的选择。但这当然值得学习和玩耍。我认为学习 Lisp 是提高编程水平的好方法,即使是其他语言也是如此。
我发现这篇文章很有趣:
这篇文章的作者 Brandon Corfman 写了一篇关于将 Java、C++ 和 Lisp 的解决方案与一个编程问题进行比较的研究,然后用 C++ 编写了他自己的解决方案。基准解决方案是 Peter Norvig 的 45 行 Lisp(用 2 小时编写)。
Corfman 发现很难将他的解决方案减少到少于 142 行 C++/STL。他对原因的分析,读起来很有趣。
我最喜欢 Lisp(和Smalltalk)系统的地方在于,它们给人一种充满活力的感觉。您可以在 Lisp 系统运行时轻松探测和修改它们。
如果这听起来很神秘,请启动Emacs并输入一些 Lisp 代码。输入C-M-x
和瞧!您刚刚从 Emacs 中更改了 Emacs。您可以在 Emacs 运行时继续并重新定义所有 Emacs 函数。
另一件事是代码=列表等价性使代码和数据之间的边界非常薄。多亏了宏,扩展语言和制作快速DSL非常容易。
例如,可以编写一个基本的 HTML 构建器,使用它的代码非常接近生成的 HTML 输出:
(html
(head
(title "The Title"))
(body
(h1 "The Headline" :class "headline")
(p "Some text here" :id "content")))
=>
<html>
<head>
<title>The title</title>
</head>
<body>
<h1 class="headline">The Headline</h1>
<p id="contents">Some text here</p>
</body>
</html>
在 Lisp 代码中,自动缩进使代码看起来像输出,除了没有任何结束标记。
我喜欢的一件事是我可以“运行时”升级代码而不会丢失应用程序状态。这只是在某些情况下有用的东西,但是当它有用时,已经存在它(或者,在开发过程中只需最低成本)比必须从头开始实现它便宜得多。尤其是因为这是以“没有甚至几乎没有”的代价。
了解如何使用 XML 模板扩展 Common Lisp:cl-quasi-quote XML 示例、项目页面、
(babel:octets-to-string
(with-output-to-sequence (*html-stream*)
<div (constantAttribute 42
someJavaScript `js-inline(print (+ 40 2))
runtimeAttribute ,(concatenate 'string "&foo" "&bar"))
<someRandomElement
<someOther>>>))
=>
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\"&foo&bar\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
这与 Lisp 的反引号阅读器(用于列表准引用)基本相同,但它也适用于各种其他内容,例如 XML(安装在特殊的 <> 语法上)、JavaScript(安装在 `js-inline 上)等.
为了清楚起见,这是在用户库中实现的!它将静态 XML、JavaScript 等部分编译成UTF-8编码的文字字节数组,准备好写入网络流。使用一个简单的,
(逗号),您可以返回到 lisp 并将运行时生成的数据交错到文字字节数组中。
这不适合胆小的人,但这就是库将上述内容编译成的内容:
(progn
(write-sequence
#(60 100 105 118 32 99 111 110 115 116 97 110 116 65 116 116 114 105 98
117 116 101 61 34 52 50 34 32 115 111 109 101 74 97 118 97 83 99 114
105 112 116 61 34 106 97 118 97 115 99 114 105 112 116 58 32 112 114
105 110 116 40 40 52 48 32 43 32 50 41 41 34 32 114 117 110 116 105
109 101 65 116 116 114 105 98 117 116 101 61 34)
*html-stream*)
(write-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-string-to-quasi-quoted-binary {1006321441}>))
(transform-quasi-quoted-string-to-quasi-quoted-binary
(let ((*transformation*
#<quasi-quoted-xml-to-quasi-quoted-string {1006326E51}>))
(locally
(declare (sb-ext:muffle-conditions sb-ext:compiler-note))
(let ((it (concatenate 'string "runtime calculated: " "&foo" "&bar")))
(if it
(transform-quasi-quoted-xml-to-quasi-quoted-string/attribute-value it)
nil))))))
*html-stream*)
(write-sequence
#(34 62 10 32 32 60 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 32 32 32 32 60 115 111 109 101 79 116 104 101 114 47
62 10 32 32 60 47 115 111 109 101 82 97 110 100 111 109 69 108 101 109
101 110 116 62 10 60 47 100 105 118 62 10)
*html-stream*)
+void+)
作为参考,上面的两个大字节向量在转换为字符串时是这样的:
"<div constantAttribute=\"42\"
someJavaScript=\"javascript: print((40 + 2))\"
runtimeAttribute=\""
第二个:
"\">
<someRandomElement>
<someOther/>
</someRandomElement>
</div>"
它与其他 Lisp 结构(如宏和函数)很好地结合在一起。现在,将其与JSP进行比较...
我喜欢这个来自http://common-lisp.net/cgi-bin/viewcvs.cgi/cl-selenium/?root=cl-selenium的宏示例 这是一个绑定到 Selenium(一个 Web 浏览器测试框架)的 Common Lisp,但是它不是映射每个方法,而是在编译时读取 Selenium 自己的 API 定义 XML 文档并使用宏生成映射代码。您可以在此处查看生成的 API:common-lisp.net/project/cl-selenium/api/selenium-package/index.html
这实质上是使用外部数据驱动宏,在这种情况下恰好是 XML 文档,但可能与从数据库或网络读取一样复杂。这就是在编译时为您提供整个 Lisp 环境的强大功能。
1970 年代,我是麻省理工学院的一名 AI 学生。和其他学生一样,我认为语言是最重要的。尽管如此,Lisp 是主要的语言。这些是我仍然认为它非常适合的一些事情:
符号数学。编写表达式的符号微分和代数化简既简单又有益。我仍然做那些,即使我在 C 中做它们。
定理证明。我时不时地进行一次临时的 AI 狂欢,比如试图证明插入排序是正确的。为此,我需要进行符号操作,而且我通常会使用 Lisp。
小的领域特定语言。我知道 Lisp 不是很实用,但是如果我想尝试一点DSL而不必完全陷入解析等问题,Lisp 宏会让它变得简单。
像极小极大博弈树搜索这样的小游戏算法可以用三行来完成。
Lisp 为我做的主要是脑力锻炼。然后我可以把它带到更实用的语言中。
PS 说到 lambda 演算,同样始于 1970 年代,在同一个 AI 领域,OO 开始侵入每个人的大脑,不知何故,对它的兴趣似乎挤掉了对它的好处的兴趣。即机器学习、自然语言、视觉、问题解决方面的工作,所有的工作都放在了房间的后面,而课程、消息、类型、多态性等都放在了前面。
你有没有看过这个解释为什么宏是强大和灵活的?抱歉,没有其他语言的示例,但它可能会向您推销宏。
@标记,
虽然你所说的有些道理,但我相信它并不总是那么直截了当。
程序员和一般人并不总是花时间评估所有可能性并决定切换语言。通常是经理决定,或者是教授第一门语言的学校……而程序员永远不需要投入足够的时间来达到一定的水平,如果他们可以决定这种语言比那种语言节省了我更多的时间。
另外,您必须承认,与没有此类支持的语言相比,有微软或 Sun 等大型商业实体支持的语言在市场上总是具有优势。
为了回答最初的问题,保罗格雷厄姆试图在这里举一个例子,尽管我承认它不一定像我想的那样实用:-)
You might find this post by Eric Normand helpful. He describes how as a codebase grows, Lisp helps by letting you build the language up to your application. While this often takes extra effort early on, it gives you a big advantage later.
它是一种多范式语言这一简单事实使其非常灵活。
John Ousterhout 在 1994 年对 Lisp 做了一个有趣的观察:
语言设计者喜欢争论为什么这种语言或那种语言 必须先验地更好或更差,但这些争论都不是很重要。最终,当用户用脚投票时,所有语言问题都会得到解决。
如果 [一种语言] 使人们更有效率,那么他们就会使用它;当出现其他更好的语言时(或者如果它已经在这里),那么人们就会切换到那种语言。这就是律法,很好。法律对我说,Scheme(或任何其他 Lisp 方言)可能不是“正确”的语言:在过去的 30 年中,有太多人用脚投票。