Lisp灵活性的实际例子?

时间:2008-09-19 22:10:33

标签: lisp homoiconicity

有人试图向我出售Lisp,作为一种超级强大的语言,可以做任何事情,然后一些。

是否有一个实用的代码示例Lisp的权力?
(最好与用常规语言编码的等效逻辑一起使用。)

18 个答案:

答案 0 :(得分:77)

我喜欢宏。

这是从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 binding”视为局部变量,它会在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代码(而不是#define在编译其他代码之前生成Lisp代码的cpp中的fluff。宏可以使用任何真正的Lisp代码,即普通函数。基本上没有限制。

摆脱丑陋

那么,让我们看看这是如何完成的。要替换一个属性,我们定义一个函数。

(defun ldap-attr (entity attr)
  `(,attr (car (ldap:attr-value ,entity ',attr))))

反引用语法看起来有点毛茸茸,但它的作用很简单。当您调用LDAP-ATTRS时,它会吐出一个包含attr的列表(即逗号),然后是car(“ list“(cons pair,实际上),实际上还有一个名为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-exps。

基本上,它允许您使用可在问题域中找到的动词编写代码,而不是语言中的低级原语!作为一个愚蠢的例子,请考虑以下内容(如果when还不是内置的)::

(defmacro my-when (test &rest body)
  `(if ,test 
     (progn ,@body)))

if是一个内置的原语,只允许你在分支中执行一个表单,如果你想拥有多个,那么你需要使用{ {1}} ::

progn

对于我们的新朋友;; one form (if (numberp 1) (print "yay, a number")) ;; two forms (if (numberp 1) (progn (assert-world-is-sane t) (print "phew!")))) ,我们可以a)使用更合适的动词,如果我们没有假分支,并且b)添加隐式排序运算符,即my-when: :

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!"))) 只进行一级扩展;有可能(实际上很可能!)扩张继续进一步下降。但是,最终你会遇到特定于编译器的实现细节,这些细节通常不是很有趣。但是继续扩展结果最终会获得更多细节,或者只是输入s-exp。

希望澄清事情。宏是一个功能强大的工具,也是我喜欢的Lisp的一个特性。

答案 1 :(得分:18)

我能想到的最好的例子是保罗格雷厄姆的书On Lisp。完整的PDF可以从我刚给出的链接下载。您也可以尝试Practical Common Lisp(也可在网络上完全获得)。

我有很多不切实际的例子。我曾经在大约40行lisp中编写了一个程序,它可以解析自己,将其源代码作为lisp列表,对列表进行树遍历,并构建一个表达式,如果waldo标识符存在于源代码中或评估为to WALDO如果沃尔多不在场,那就没有了。返回的表达式是通过将对car / cdr的调用添加到已解析的原始源来构造的。我不知道如何用40行代码在其他语言中做到这一点。也许perl可以用更少的行来完成它。

答案 2 :(得分:14)

您可能会发现此文章有用:http://www.defmacro.org/ramblings/lisp.html

也就是说,提供Lisp功能的简短实用例子是非常非常困难的,因为它只在非平凡的代码中闪耀。当你的项目增长到一定规模时,你会欣赏Lisp的抽象设施,并且很高兴你一直在使用它们。另一方面,合理的短代码样本永远不会给你一个令人满意的演示,因为Lisp很棒,因为其他语言的预定义缩写在小例子中看起来比Lisp在管理特定于域的抽象中的灵活性更具吸引力。

答案 3 :(得分:13)

我喜欢Common Lisp Object System(CLOS)和多方法。

大多数(如果不是全部)面向对象编程语言都具有类和方法的基本概念。 Python中的以下片段定义了PeelingTool和Vegetable类(类似于访问者模式):

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等功能,可以让您快速轻松地获得结果,而无需跳过如此多的箍,您可以解决这个问题,或者发现自己变成了袋鼠。

答案 4 :(得分:13)

实际上,一个很好的实际例子是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是推动编程的好方法,即使在其他语言中也是如此。

答案 5 :(得分:12)

Lisp中有很多杀手锏,但宏是我特别喜欢的,因为在语言定义和我定义的内容之间不再存在障碍。例如,Common Lisp没有 while 构造。我曾经在走路时把它实施在我脑海里。它简单明了:

(defmacro while (condition &body body)
  `(if ,condition
       (progn
         ,@body
         (do nil ((not ,condition))
           ,@body))))

Etvoilà!您刚刚使用新的基本结构扩展了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语言执行此操作仍然是读者的练习......

答案 6 :(得分:9)

我发现这篇文章非常有趣:

Programming Language Comparison: Lisp vs C++

文章的作者Brandon Corfman写了一篇研究,将Java,C ++和Lisp中的解决方案与编程问题进行比较,然后用C ++编写自己的解决方案。基准解决方案是Peter Norvig的45行Lisp(用2小时编写)。

Corfman发现很难将他的解决方案减少到少于142行的C ++ / STL。他对原因的分析是一个有趣的读物。

答案 7 :(得分:8)

我最喜欢Lisp(和Smalltalk)系统的东西是他们觉得活着。你可以很容易地探测和在Lisp系统运行时修改它们。

如果这听起来很神秘,请启动Emacs,然后键入一些Lisp代码。输入C-M-x并vo!您刚刚从Emacs中更改了Emacs。您可以在运行时继续并重新定义所有Emacs功能。

另一件事是代码=列表等价使得代码和数据之间的边界非常薄。感谢宏,很容易扩展语言并快速DSLs

例如,可以编写一个基本的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代码中,自动缩进使代码看起来像输出,除了没有任何结束标记。

答案 8 :(得分:7)

了解如何使用XML模板扩展Common Lisp cl-quasi-quote XML exampleproject page

(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=\"&amp;foo&amp;bar\">
    <someRandomElement>
      <someOther/>
    </someRandomElement>
  </div>"

这与Lisp的反引号读取器(用于列表准引用)基本相同,但它也适用于各种其他内容,如XML(安装在特殊的&lt;&gt;语法),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结构(如宏和函数)结合得很好。现在,将其与JSPs ...

进行比较

答案 9 :(得分:7)

我是20世纪70年代麻省理工学院的AI学生。像其他学生一样,我认为语言是最重要的。然而,Lisp是主要语言。这些是我仍然认为非常适合的一些事情:

  • 符号数学。编写表达式的符号区分和代数简化是容易和有益的。我仍然会这样做,即使我是用C-whatever做的。

  • 定理证明。现在每一个&amp;然后我继续进行临时AI狂欢,就像试图证明插入排序是正确的一样。为此,我需要进行符号操作,我通常会依赖于Lisp。

  • 少量特定领域的语言。我知道Lisp不是真的实用,但是如果我想尝试一点DSL而不必将所有内容都解压缩等等,那么Lisp宏可以让它变得简单。< / p>

  • 像minimax游戏树搜索这样的小游戏算法可以用三行来完成。

  • 想试试lambda calculus?在Lisp中很容易。

Lisp主要为我做的是心理锻炼。然后我可以把它带到更实用的语言中。

P.S。说到lambda演算,也是在20世纪70年代开始的那个同样的AI millieu,OO开始侵入每个人的大脑,并且不知何故,对 的兴趣似乎已经挤出了很多兴趣它有好处。即从事机器学习,自然语言,视觉,解决问题的工作,各种各样的课程,消息,类型,多态等等都走到了房间的后面。

答案 10 :(得分:7)

我喜欢来自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环境的强大功能。

答案 11 :(得分:6)

我喜欢的一件事是我可以在不丢失应用程序状态的情况下升级代码“运行时”。这在某些情况下是有用的,但是当它有用时,将它放在那里(或者,在开发期间只需要很少的成本)比从头开始实现它要便宜得多。特别是因为这是“几乎没有”的成本。

答案 12 :(得分:5)

您是否看过this解释为什么宏强大且灵活的原因?但是,没有其他语言的例子,抱歉,但它可能会在宏上卖给你。

答案 13 :(得分:4)

@马克,

虽然你所说的有一些道理,但我认为并不总是那么直截了当。

程序员和一般人并不总是花时间评估所有可能性并决定切换语言。经常是由经理决定,或者是教授第一语言的学校...而程序员从来没有必要投入足够的时间来达到某个水平,因为他们可以决定这种语言比这种语言节省更多的时间。

另外,您必须承认,与没有此类支持的语言相比,拥有大型商业实体(如Microsoft或Sun)支持的语言在市场上始终具有优势。

为了回答原始问题,保罗格雷厄姆试图给出一个例子here,尽管我承认它不一定像我想的那样实用: - )

答案 14 :(得分:4)

令我印象深刻的一件事是能够编写自己的面向对象的编程扩展,如果您不喜欢包含的CLOS。

其中一个在Garnet,一个在保罗格雷厄姆的On Lisp

还有一个名为Screamer的包允许非确定性编程(我还没有评估)。

任何允许您更改它以支持不同编程范例的语言都必​​须具有灵活性。

答案 15 :(得分:2)

你可能会发现Eric Normand的this post很有帮助。他描述了如何在代码库增长的过程中,Lisp通过让您根据应用程序构建语言来提供帮助。虽然这在早期经常需要额外的努力,但它会在以后给你带来很大的好处。

答案 16 :(得分:2)

这是一种多范式语言的简单事实使它非常灵活。

答案 17 :(得分:-1)

John Ousterhout在1994年对Lisp做了一个有趣的观察:

  

语言设计师喜欢争论为什么这种语言或那种语言   必须先验更好或更差,但这些论点都不是真的   很重要。最终,当用户投票时,所有语言问题都会得到解决   用脚。

     

如果[某种语言]能让人们提高工作效率,那么他们就会使用   它;当一些其他语言出现时更好(或者如果是的话)   在这里,人们将切换到那种语言。这是   法律,这是好的。法律告诉我Scheme(或任何其他Lisp   方言)可能不是“正确”的语言:人太多了   在过去的30年里,他们用脚投票。

http://www.vanderburg.org/OldPages/Tcl/war/0009.html