如何理解 Lisp/Clojure 代码

非常感谢所有漂亮的回答! 不能只标记一个正确

注意: 已经是维基百科了

我是函数式编程的新手,虽然我能读懂函数式编程中的简单函数,例如计算一个数的阶乘,但我发现很难读懂大函数。 部分原因是因为我无法在函数定义中找出更小的代码块,还有一部分原因是因为在代码中匹配 ( )变得越来越困难。

如果有人能指导我阅读一些代码,并给我一些如何快速破译一些代码的提示,那就太好了。

注意: 如果我盯着它看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))))
9873 次浏览

首先要记住,函数式程序由表达式组成,而不是语句。例如,表单 (if condition expr1 expr2)将其第一个参数作为条件来测试布尔值的值,并对其进行计算,如果它的计算结果为 true,那么它将计算并返回 expr1,否则将计算并返回 expr2。当每个表单返回一个表达式时,一些常见的语法结构,如 THEN 或 ELSE 关键字可能就会消失。请注意,这里 if本身也计算为一个表达式。

关于求值: 在 Clojure (和其他 Lisps)中,你遇到的大多数表单都是 (f a1 a2 ...)形式的函数调用,在实际的函数调用之前,对 f的所有参数都进行了求值; 但是表单也可以是宏或者特殊的表单,它们不会对其中的一些(或全部)参数进行求值。如果有疑问,请参考文档 (doc f)或者只是检查 REPL:

User = > application 用户 = > 申请
# < core $Apply _ _ 3243 clojure.core $Apply _ _ 3243@19bb5c09 & gt;
a function < br > User = > doseq
例外: 不能获取宏的值: # ‘ clojure.core/doseq
a Macro.

这两条规则:

  • 我们有表达式,不是陈述式
  • 根据外部形式的行为,子形式的评估可能会发生,也可能不会发生

应该可以减轻您对 Lisp 程序的学习,特别是如果它们像您给出的例子一样有很好的缩进。

希望这个能帮上忙。

特别是 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 没有太大的变化。

我认为 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. If you put your cursor on a paren and your text editor doesn't syntax-highlight the matching one, I suggest you find a new editor.

  3. Sometimes it helps to read code inside-out. Clojure code tends to be deeply nested.

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

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

    好: “好的,我们来取一些 xs。ABC1的意思是偶数的反义词,所以是“奇数”。所以我们筛选了一些集合,只留下奇数。然后我们把它们除以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 中拥有可用的文档的有用性怎么强调都不为过。如果您使用 clojure.contrib.repl-utils并且具有。Clj 文件上的类路径,你可以做 (source some-function),并看到它的所有源代码。您可以执行 (show some-java-class)并查看其中所有方法的说明。诸如此类。

只有经验丰富才能快速阅读。Lisp 并不比其他语言更难读。碰巧大多数语言看起来像 C,大多数程序员花费大部分时间阅读它,所以 C 语法看起来更容易阅读。练习练习。