2009-12-12 106 views
70

非常感谢所有美丽的答案!不能标记只有一个是正确的如何阅读心理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)))) 
+3

体验?你习惯于阅读Lisp代码会更快。然而,关于Lisp的主要抱怨之一是很难阅读,所以不要期望它很快变得直观。 – 2009-12-12 18:39:57

+10

这不是一个简单的功能。如果10分钟后你能完全理解它,那就很好。 – 2009-12-12 22:57:58

回答

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,但大多数规则应该是相同的,因为其他Lisp没有太多差别。

+0

在你的第二个代码示例中,我如何控制第五行的缩进,即(Emacs中的lazy-seq?),默认情况下,它与前一行的'y'对齐 – 2009-12-31 20:52:22

+0

对不起,我不知道。我不使用clojure,这个例子不能精确地转换成Scheme。你可能会检查一个像clojure-indent-offset这样的变量。例如,对于Haskell,我必须添加'(haskell-indent-offset 2)我的自定义设置变量 – 2010-01-04 01:29:37

+0

这里是Clojure的风格指南https://github.com/bbatsov/clojure-style-guide – 2016-09-30 15:15:08

7

首先要记住,功能性程序包括表情,不发言。例如,表格(if condition expr1 expr2)将其第一个参数作为条件来测试布尔值,评估它,如果它被评估为真,则评估并返回expr1,否则评估并返回expr2。当每个表单返回一个表达式时,像THEN或ELSE关键字这样的常见语法结构可能会消失。请注意,这里if本身也评估为表达式。

现在关于评估:在Clojure(和其他Lisp)中,您遇到的大多数表格都是函数调用形式(f a1 a2 ...),其中f的所有参数在实际函数调用之前被求值;但是表单也可以是宏或者不评估其一些(或全部)参数的特殊形式。如有疑问,请查阅文档(doc f)或只是检查REPL:

user=> apply
#<core$apply__3243 [email protected]>
功能
user=> doseq
java.lang.Exception: Can't take value of a macro: #'clojure.core/doseq
宏。

这两个规则:

  • 我们有表达,而不是报表
  • 可能会出现子窗体的
  • 评估与否,取决于如何外在形式表现

应该减轻你的Lisp的groking程序,尤其是。如果他们像你给的例子一样有很好的缩进。

希望这会有所帮助。

58

我认为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提供正确的文档是非常有用的,这一点也不为过。如果您使用clojure.contrib.repl-utils并在类路径中包含.clj文件,则可以执行(source some-function)并查看其所有源代码。你可以做(show some-java-class)并查看其中所有方法的描述。等等。

能够快速阅读的东西只有经验。 Lisp并不比任何其他语言更难阅读。恰巧大多数语言看起来像C,大多数程序员花费大部分时间阅读它,所以看起来C语言更容易阅读。练习练习练习。

+3

+1提到'源'功能,这一点的信息很难来通过clojure.contrib.repl-utils的其余部分来查看我错过了什么 – vedang 2010-11-22 14:50:00

+3

+1将中间结果放入let中,使得代码更具可读性(对于Clojure新手) – 2014-03-04 16:26:53