Lisp灵活性的实际例子?
有人试图将Lisp卖给我,这是一种超级强大的语言,可以处理所有事情,然后再做一些。
有Lisp功能强大的实用代码示例吗?
(最好与以常规语言编码的等效逻辑一起使用。)
解决方案
我们是否看过有关为什么宏功能强大且灵活的解释?抱歉,没有其他语言的示例,但是它可能会在宏上卖给我们。
我能想到的最好的例子是保罗·格雷厄姆(Paul Graham)在《 Lisp》上的书。完整的PDF可以从我刚刚给出的链接下载。我们也可以尝试使用"实用通用Lisp"(也可以在网上完全获得)。
我有很多不切实际的例子。我曾经用大约40行lisp编写了一个程序,该程序可以对其进行解析,将其源视为lisp列表,对列表进行树遍历并构建一个表达式,如果该源中存在waldo标识符,则该表达式将评估为WALDO或者评估为如果不存在瓦尔多,则为零。通过将对car / cdr的调用添加到已解析的原始源中来构造返回的表达式。我不知道如何用40行代码用其他语言执行此操作。也许perl可以用更少的行来做到这一点。
John Ousterhout在1994年对Lisp提出了一个有趣的观察:
Language designers love to argue about why this language or that language must be better or worse a priori, but none of these arguments really matter a lot. Ultimately all language issues get settled when users vote with their feet. If [a language] makes people more productive then they will use it; when some other language comes along that is better (or if it is here already), then people will switch to that language. This is The Law, and it is good. The Law says to me that Scheme (or any other Lisp dialect) is probably not the "right" language: too many people have voted with their feet over the last 30 years.
http://www.vanderburg.org/OldPages/Tcl/war/0009.html
我们可能会发现这篇文章很有帮助:http://www.defmacro.org/ramblings/lisp.html
就是说,很难,简短地给出Lisp功能强大的实际示例,因为Lisp仅在非平凡的代码中才真正发挥作用。当项目增长到一定规模时,我们将欣赏Lisp的抽象工具,并为我们使用它们感到高兴。另一方面,合理的短代码示例将永远不会给我们令人满意的演示,说明Lisp的出色之处,因为其他语言的预定义缩写在小示例中看起来比Lisp在管理特定于领域的抽象上的灵活性更具吸引力。
@标记,
虽然我们说的话有些道理,但我相信这并不总是那么简单。
程序员和人们通常并不总是花时间评估所有可能性并决定切换语言。通常是由经理决定,还是由学校教授第一门语言……程序员根本不需要花费足够的时间来达到特定水平,因为他们可以决定使用这种语言比使用这种语言可以为我节省更多时间。
另外,我们必须承认,与没有大型语言的商业实体(例如Microsoft或者Sun)支持的语言相比,具有这种语言的语言在市场上将始终具有优势。
为了回答最初的问题,保罗·格雷厄姆(Paul Graham)尝试在此处举一个例子,尽管我承认它不一定像我想要的那样实用:-)
我们可能会发现Eric Normand的这篇文章很有帮助。他描述了随着代码库的增长,Lisp可以根据应用程序构建语言,从而为我们提供帮助。尽管这通常需要尽早进行额外的努力,但稍后会为我们带来很大的好处。
实际上,一个很好的实际例子是Lisp LOOP Macro。
http://www.ai.sri.com/pkarp/loop.html
LOOP宏只是-a Lisp宏。然而,它基本上定义了一个小型循环DSL(域特定语言)。
当我们浏览该小教程时,我们会发现(即使是新手)也很难知道代码的哪一部分是Loop宏的一部分,而哪一部分是"正常的" Lisp。
这是Lisps表达能力的关键组成部分之一,新的代码实际上无法与系统区分开。
例如,在使用Java时,我们可能(乍看之下)无法知道程序的哪一部分来自标准Java库,而不是我们自己的代码,甚至是第三方库,我们也确实知道代码的哪一部分是Java语言,而不是简单地对类进行方法调用。当然,所有这些都是" Java语言",但是作为程序员,我们仅限于将应用程序表示为类和方法(以及现在的注释)的组合。而在Lisp中,几乎所有内容都可以争夺。
考虑将Common Lisp连接到SQL的Common SQL接口。在这里,http://clsql.b9.com/manual/loop-tuples.html展示了如何扩展CL Loop宏以使SQL绑定成为"一流公民"。
我们还可以观察到诸如" [选择[名字] [姓氏]:从[员工] ::按[姓氏]排列]]"的构造。这是CL-SQL程序包的一部分,并实现为"阅读器宏"。
在Lisp中,我们不仅可以制作宏来创建新的结构,例如数据结构,控制结构等。而且我们甚至可以通过阅读器宏来更改语言的语法。在这里,他们使用读取器宏(在这种情况下为'['符号)进入SQL模式,以使SQL像嵌入式SQL一样工作,而不是像许多其他语言一样仅用作原始字符串。
作为应用程序开发人员,我们的任务是将我们的流程和构造转换为处理器可以理解的形式。这意味着我们不可避免地不得不"谈论"计算机语言,因为它"无法理解"我们。
Common Lisp是为数不多的环境之一,在该环境中,我们不仅可以自上而下地构建应用程序,还可以提升语言和环境,以使我们半途而废。我们可以在两端进行编码。
介意,尽其优雅,这不是万能药。显然,还有其他因素会影响语言和环境选择。但这当然值得学习和使用。我认为学习Lisp是提高编程水平的好方法,即使使用其他语言也是如此。
我喜欢宏。
这是用于从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 ++中发现的完全不同:在这里,我们可以运行生成Lisp代码的实际Lisp代码(而不是cpp中的#define绒毛)。 ,然后再编译其他代码。宏可以使用任何真实的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))
,@
语法是将列表的内容放在某个地方,而不是实际的列表。
我们可以轻松地验证这将为我们提供正确的东西。宏通常以这种方式编写:首先从要简化的代码(输出)开始,到要编写的代码(输入)开始,然后开始模制宏,直到输入提供正确的输出为止。函数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绑定与开头的代码进行比较,我们会发现它的格式相同!
宏是在编译时运行的代码,其添加的功能是可以随意调用任何普通函数或者宏!它只不过是一个花哨的过滤器,它接受了一些参数,应用了一些转换,然后向编译器提供了生成的s-exp。
基本上,它使我们可以使用可以在问题域中找到的动词来编写代码,而不是使用该语言中的低级原语!作为一个愚蠢的例子,请考虑以下内容(如果" when"还不是内置的):
(defmacro my-when (test &rest body) `(if ,test (progn ,@body)))
如果是内置的原语,则只允许我们在分支中执行一种形式,如果我们希望拥有多个以上形式,则需要使用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功能之一。
我喜欢通用Lisp对象系统(CLOS)和多方法。
大多数(如果不是全部)面向对象的编程语言都具有类和方法的基本概念。 Python中的以下代码段定义了PeelingTool和Vegetable类(类似于Visitor模式):
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)
我们将Peeling方法放入PeelingTool中,并让Banana接受它。但是,它必须属于PeelingTool类,因此仅当我们具有PeelingTool类的实例时才可以使用它。
通用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(和Smalltalk)系统的地方是,它们感觉很活跃。我们可以在Lisp系统运行时轻松地对其进行探测和修改。
如果这听起来很神秘,请启动Emacs,然后键入一些Lisp代码。输入C-M-x
然后贴上!我们只是从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-准引用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进行比较...
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语言来完成此操作...
我发现这篇文章很有趣:
编程语言比较:Lisp与C ++
本文的作者Brandon Corfman写了一篇研究,该研究将Java,C ++和Lisp中的解决方案与编程问题进行了比较,然后编写了自己的C ++解决方案。基准解决方案是Peter Norvig的45行Lisp(在2小时内编写)。
Corfman发现很难将其解决方案减少到少于142行C ++ / STL。他对原因的分析很有趣。
我喜欢的一件事是我可以在不丢失应用程序状态的情况下升级代码"运行时"。它仅在某些情况下有用,但是当它有用时,已经存在(或者在开发过程中仅花费最小的成本)要比从头开始实现便宜得多。尤其是因为这要付出"几乎没有"的代价。
它是一种多范式语言这一简单事实使其变得非常灵活。
我在1970年代是麻省理工学院的AI学生。像其他每个学生一样,我认为语言是至高无上的。尽管如此,Lisp是主要语言。这些是我仍然认为非常适合的一些事项:
- 符号数学。编写表达式的符号差异和代数简化是容易且有启发性的。即使我用C语言做它们,我仍然会做那些。
- 定理证明。我不时地进行临时的AI狂欢,就像试图证明插入排序是正确的那样。为此,我需要进行符号操作,通常我会回到Lisp。
- 特定领域的语言很少。我知道Lisp并不是很实用,但是如果我想尝试一点DSL而不必全都解析等,那么Lisp宏将使它变得容易。
- 像minimax游戏树搜索之类的小游戏算法可以在三行中完成。
- 想尝试lambda演算吗?在Lisp中很容易。
Lisp为我做的主要是心理锻炼。然后,我可以将其推广到更实用的语言中。
P.S.说到lambda演算,也是从1970年代开始的那个相同的AI千分之一,就是OO开始入侵每个人的大脑,并且某种程度上,对它的兴趣似乎已经排挤了对它的好处的兴趣。 IE。机器学习,自然语言,视觉,问题解决之类的工作全都排到了房间的后面,而课堂,消息,类型,多态性等则排在了最前面。
让我印象深刻的一件事是,如果我们碰巧不喜欢随附的CLOS,则可以编写自己的面向对象的程序扩展。
其中之一在石榴石中,另一在Paul Graham的《 On Lisp》中。
还有一个名为Screamer的软件包,它允许进行不确定性编程(我尚未评估)。
任何允许我们对其进行更改以支持不同编程范例的语言都必须具有灵活性。
我喜欢来自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环境的力量。