program story

정신적으로 Lisp / Clojure 코드를 읽는 방법

inputbox 2020. 10. 27. 08:09
반응형

정신적으로 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))))

특히 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. I don't know Clojure, but most of the rules should be the same since none of the other Lisps vary much.


I think concat is a bad example to try to understand. It's a core function and it's more low-level than code you would normally write yourself, because it strives to be efficient.

Another thing to keep in mind is that Clojure code is extremely dense compared to Java code. A little Clojure code does a lot of work. The same code in Java would not be 23 lines. It would likely be multiple classes and interfaces, a great many methods, lots of local temporary throw-away variables and awkward looping constructs and generally all kinds of boilerplate.

Some general tips though...

  1. Try to ignore the parens most of the time. Use the indentation instead (as Nathan Sanders suggests). 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))))
    

    When I look at that my brain sees:

    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))))
    

    Bad: "So we start with numbers from 1 to 10. Then we're reversing the order of the mapping of the filtering of the complement of the wait I forgot what I'm talking about."

    Good: "OK, so we're taking some xs. (complement even?) means the opposite of even, so "odd". So we're filtering some collection so only the odd numbers are left. Then we're dividing them all by 17. Then we're reversing the order of them. And the xs in question are 1 to 10, gotcha."

    Sometimes it helps to do this explicitly. Take the intermediate results, throw them in a let and give them a name so you understand. The REPL is made for playing around like this. Execute the intermediate results and see what each step gives you.

    (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)
    

    Soon you will be able to do this sort of thing mentally without effort.

  4. Make liberal use of (doc). The usefulness of having documentation available right at the REPL can't be overstated. If you use clojure.contrib.repl-utils and have your .clj files on the classpath, you can do (source some-function) and see all the source code for it. You can do (show some-java-class) and see a description of all the methods in it. And so on.

Being able to read something quickly only comes with experience. Lisp is no harder to read than any other language. It just so happens that most languages look like C, and most programmers spend most of their time reading that, so it seems like C syntax is easier to read. Practice practice practice.


First remember that functional program consists of expressions, not statements. For example, form (if condition expr1 expr2) takes its 1st arg as a condition to test for the boolean falue, evaluates it, and if it eval'ed to true then it evaluates and returns expr1, otherwise evaluates and returns expr2. When every form returns an expression some of usual syntax constructs like THEN or ELSE keywords may just disappear. Note that here if itself evaluates to an expression as well.

Now about the evaluation: In Clojure (and other Lisps) most forms you encounter are function calls of the form (f a1 a2 ...), where all arguments to f are evaluated before actual function call; but forms can be also macros or special forms which don't evaluate some (or all) of its arguments. If in doubt, consult the documentation (doc f) or just check in REPL:

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

These two rules:

  • we have expressions, not statements
  • evaluation of a subform may occur or not, depending of how outer form behaves

should ease your groking of Lisp programs, esp. if they have nice indentation like the example you gave.

Hope this helps.

참고URL : https://stackoverflow.com/questions/1894209/how-to-read-mentally-lisp-clojure-code

반응형