如何阅读精神上的Lisp / Clojure代码

时间:2009-12-12 17:58:07

标签: clojure lisp

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

注意:已经是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 个答案:

答案 0 :(得分:57)

我认为concat是一个不好的例子,试图理解。它是一个核心功能,它比你通常自己编写的代码更低级,因为它努力提高效率。

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

虽然有一些一般的提示......

  1. 尝试在大多数情况下忽略parens。改为使用缩进(正如Nathan Sanders建议的那样)。 e.g。

    (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. 如果你把光标放在paren上,而你的文本编辑器没有语法高亮显示匹配的那个,我建议你找一个新的编辑器。

  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上提供文档的有用性不容小觑。如果您使用clojure.contrib.repl-utils并在类路径上包含.clj文件,则可以执行(source some-function)并查看其所有源代码。您可以执行(show some-java-class)并查看其中所有方法的说明。等等。

  5. 能够快速阅读的东西只有经验。 Lisp并不比任何其他语言更难阅读。事实上,大多数语言看起来都像C,大多数程序员大部分时间都在阅读,所以看起来C语法更容易阅读。实践练习。

答案 1 :(得分:47)

由于常规语法,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,你知道你有三个街区,因为同一级别有三件事。但是第三行以下的所有内容都在它下面缩进,所以其余部分属于第三个块。

Here is a style guide for Scheme。我不知道Clojure,但大多数规则都应该是相同的,因为其他Lisps都没有太大差异。

答案 2 :(得分:7)

首先请记住,功能程序由表达式而非语句组成。例如,表单(if condition expr1 expr2)将其第一个arg作为测试布尔值的条件,对其进行求值,如果它被eval'为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程序的研究,特别是如果他们有很好的缩进,就像你给出的例子一样。

希望这有帮助。