如何看懂Lisp / Clojure代码


76

非常感谢所有美丽的答案!无法仅将其中一个标记为正确

注意:已经是Wiki

我是函数编程的新手,虽然我可以阅读函数式编程中的简单函数,例如,计算数字的阶乘,但我发现很难阅读大函数。部分原因是因为我无法找出函数定义中较小的代码块,部分原因是因为我很难( )在代码中进行匹配。

如果有人可以引导我阅读一些代码并提供一些有关如何快速解密某些代码的提示,那将是很好的。

注意:如果我盯着它看10分钟,我就能理解该代码,但是我怀疑如果用Java编写相同的代码,那将花费我10分钟。因此,我认为要熟悉Lisp风格的代码,我必须更快地做到这一点

注意:我知道这是一个主观问题。我在这里没有寻求任何可证明的正确答案。只是关于您如何阅读此代码的注释,将受到欢迎并且非常有帮助

(defn concat
  ([] (lazy-seq nil))
  ([x] (lazy-seq x))
  ([x y]
    (lazy-seq
      (let [s (seq x)]
        (if s
          (if (chunked-seq? s)
            (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
            (cons (first s) (concat (rest s) y)))
          y))))
  ([x y & zs]
     (let [cat (fn cat [xys zs]
                 (lazy-seq
                   (let [xys (seq xys)]
                     (if xys
                       (if (chunked-seq? xys)
                         (chunk-cons (chunk-first xys)
                                     (cat (chunk-rest xys) zs))
                         (cons (first xys) (cat (rest xys) zs)))
                       (when zs
                         (cat (first zs) (next zs)))))))]
       (cat (concat x y) zs))))

3
经验?习惯于阅读Lisp代码的人会更快。但是,关于Lisp的主要抱怨之一是它很难阅读,所以不要指望它会很快对您变得直观。
Nate CK

10
这不是一个简单的功能。如果十分钟后您可以完全理解它,那很好。
米歇尔·德马雷

Answers:


49

尤其是Lisp代码,由于规则的语法,甚至比其他功能语言更难阅读。Wojciech为改善您的语义理解提供了一个很好的答案。这是一些语法帮助。

首先,在阅读代码时,不要担心括号。担心缩进。一般规则是,相同缩进级别的事物是相关的。所以:

      (if (chunked-seq? s)
        (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
        (cons (first s) (concat (rest s) y)))

其次,如果您不能将所有内容都放在一行上,请缩进下一行。这几乎总是两个空格:

(defn concat
  ([] (lazy-seq nil))  ; these two fit
  ([x] (lazy-seq x))   ; so no wrapping
  ([x y]               ; but here
    (lazy-seq          ; (lazy-seq indents two spaces
      (let [s (seq x)] ; as does (let [s (seq x)]

第三,如果某个函数的多个参数不能放在一行上,则将第二,第三等参数放在第一个起始括号的下面。许多宏都有类似的规则,但有一些变化,以允许重要部分首先出现。

; fits on one line
(chunk-cons (chunk-first s) (concat (chunk-rest s) y))

; has to wrap: line up (cat ...) underneath first ( of (chunk-first xys)
                     (chunk-cons (chunk-first xys)
                                 (cat (chunk-rest xys) zs))

; if you write a C-for macro, put the first three arguments on one line
; then the rest indented two spaces
(c-for (i 0) (< i 100) (add1 i)
  (side-effects!)
  (side-effects!)
  (get-your (side-effects!) here))

这些规则可帮助您在代码中查找代码块:

(chunk-cons (chunk-first s)

不要计算括号!检查下一行:

(chunk-cons (chunk-first s)
            (concat (chunk-rest s) y))

您知道第一行不是完整的表达式,因为下一行在其下方缩进。

如果defn concat从上方看,您会知道您有三个街区,因为在同一级别上有三个东西。但是第三行下方的所有内容都在其下方缩进,因此其余部分属于该第三区块。

这是Scheme的样式指南。我不了解Clojure,但是大多数规则应该是相同的,因为其他Lisps差异都不大。


在您的第二个代码示例中,如何控制第五行的缩进,即(在Emacs中为lazy-seq?默认情况下,它与上一行的y对齐。)
Wei Hu

对不起,我不知道。我不使用clojure,并且该示例也不能精确地转换为Scheme。您可能会检查像clojure-indent-offset这样的变量。例如,对于Haskell,我必须将'((haskell-indent-offset 2)添加到我的自定义集合变量中。
Nathan Shively-Sanders 2010年

59

我认为这concat是一个很难理解的例子。它是一个核心功能,比您通常自己编写的代码更底层,因为它努力提高效率。

要记住的另一件事是,与Java代码相比,Clojure代码非常密集。一点Clojure代码可以完成很多工作。Java中的相同代码不会是23行。它可能是多个类和接口,许多方法,许多本地临时丢弃变量和笨拙的循环构造以及通常的所有样板。

一些一般性提示...

  1. 在大多数情况下,请尽量忽略括号。请改用缩进(如Nathan Sanders建议的那样)。例如

    (if s
      (if (chunked-seq? s)
        (chunk-cons (chunk-first s) (concat (chunk-rest s) y))
        (cons (first s) (concat (rest s) y)))
      y))))
    

    当我看时,我的大脑会看到:

    if foo
      then if bar
        then baz
        else quux
      else blarf
    
  2. 如果将光标放在括号上,而文本编辑器没有语法突出显示匹配的文本,则建议您找到一个新的编辑器。

  3. 有时,它有助于从内而外读取代码。Clojure代码倾向于深层嵌套。

    (let [xs (range 10)]
      (reverse (map #(/ % 17) (filter (complement even?) xs))))
    

    坏: “所以我们从1到10开始。然后我们颠倒了我忘记了我在说什么的等待补码过滤的映射顺序。”

    好:“好,所以我们取一些xs(complement even?)表示偶数的反义词,所以取“奇数”。所以我们要过滤一些集合,只剩下奇数。然后将它们全部除以17。然后“将它们的顺序颠倒了。xs问题是1到10,知道了。”

    有时它有助于明确地执行此操作。取中间结果,将其放入let并命名,以使您理解。REPL是为像这样玩而设计的。执行中间结果,看看每个步骤都能给您带来什么。

    (let [xs (range 10)
          odd? (complement even?)
          odd-xs (filter odd? xs)
          odd-xs-over-17 (map #(/ % 17) odd-xs)
          reversed-xs (reverse odd-xs-over-17)]
      reversed-xs)
    

    很快,您将无需费力就能在脑子上做这种事情。

  4. 充分利用(doc)。在REPL上拥有可用文档的有用性不容小stat。如果您使用clojure.contrib.repl-utils.clj文件并将其放在类路径中,则可以(source some-function)查看该文件的所有源代码。您可以执行(show some-java-class)并查看其中所有方法的描述。等等。

能够快速阅读某些东西只是经验。Lisp不比其他任何语言都难读。碰巧的是,大多数语言看起来像C,并且大多数程序员花费大量时间来阅读它,因此C语言语法似乎更易于阅读。练习练习。


3
+1代表“源”功能,那一点信息比原本应该的要难得多。现在遍历clojure.contrib.repl-utils的其余部分,看看我错过了什么。
吠陀

3
+1可以让中间结果抛出,使代码更具可读性(对于Clojure新手而言)
Greg K

确实应该有人写一些文章,甚至是写一本关于如何作为非LISP程序员学习LISP的书(例如,来自Python)。似乎是如此不同,以至于我们非LISP人士都需要学习如何学习LISP。经过这种解释,似乎并不太难理解。以前,我看过LISP代码,然后想到:“尝试的目的是什么?”。
bob

有人知道他们会建议的任何这类书吗?
鲍勃

7

首先请记住,功能程序由表达式而不是语句组成。例如,form(if condition expr1 expr2) 将其第一个arg作为条件来测试布尔值,然后对其求值,如果将其评估为true,则它求值并返回expr1,否则求值并返回expr2。当每种形式返回一个表达式时,一些常用的语法构造(例如THEN或ELSE关键字)可能会消失。请注意,这里if本身也计算表达式。

现在开始评估:在Clojure(和其他Lisps)中,您遇到的大多数形式都是形式的函数调用(f a1 a2 ...),其中所有要调用的参数f都在实际函数调用之前进行评估。但是表单也可以是不评估其某些(或全部)自变量的宏或特殊形式。如有疑问,请查阅文档(doc f)或仅签入REPL:

user=> apply
#<core$apply__3243 clojure.core$apply__3243@19bb5c09>
函数宏。
user=> doseq
java.lang.Exception: Can't take value of a macro: #'clojure.core/doseq

这两个规则:

  • 我们有表达,没有陈述
  • 是否评估子表单,取决于外部表单的行为

特别是应该可以简化Lisp程序的使用。如果它们像您给出的示例那样具有很好的缩进。

希望这可以帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.