Как читать мысленно 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))))

Ответ 1

Lisp, в частности, еще труднее читать, чем другие функциональные языки из-за регулярного синтаксиса. Войцех дает хороший ответ для улучшения вашего смыслового понимания. Вот несколько советов по синтаксису.

Во-первых, при чтении кода не беспокойтесь о круглых скобках. Беспокойство об отступе. Общее правило заключается в том, что вещи с одинаковым уровнем отступа связаны. Итак:

      (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 сверху, вы знаете, что у вас есть три блока, потому что на одном уровне есть три вещи. Но под третьей строкой все под ним подрезано, так что остальное принадлежит этому третьему блоку.

Вот руководство по стилю для схемы. Я не знаю Clojure, но большинство правил должны быть одинаковыми, поскольку ни один из других Lisps не сильно отличается.

Ответ 2

Я думаю, что concat - плохой пример, чтобы попытаться понять. Это основная функция, и она более низкоуровневая, чем код, который вы обычно пишете сами, потому что он стремится быть эффективным.

Еще одна вещь, о которой нужно помнить, заключается в том, что код Clojure чрезвычайно плотный по сравнению с Java-кодом. Небольшой код Clojure выполняет большую работу. Тот же код в Java не будет 23 строк. Вероятно, это будет несколько классов и интерфейсов, множество методов, множество локальных временных отбрасывающих переменных и неудобные петлевые конструкции и, как правило, все виды шаблонов.

Некоторые общие советы, хотя...

  • Попытка игнорировать parens большую часть времени. Вместо этого используйте отступы (как предлагает Натан Сандерс). например.

    (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
    
  • Если вы поместите курсор на паз, и ваш текстовый редактор не синтаксис, выделите соответствующий, я предлагаю вам найти новый редактор.

  • Иногда это помогает читать код наизнанку. Clojure код имеет тенденцию быть глубоко вложенным.

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

    Плохо: "Итак, мы начинаем с чисел от 1 до 10. Затем мы меняем порядок отображения фильтрации дополнения ожидания, я забыл, что я говорю".

    Хорошо: "Итак, мы берем несколько xs. (complement even?) означает противоположность четному, поэтому" нечетное ".Так что мы фильтруем некоторую коллекцию, так что осталось только нечетные числа. re деля их все на 17. Затем мы меняем порядок их. И xs в вопросе от 1 до 10, gotcha."

    Иногда это помогает сделать это явно. Возьмите промежуточные результаты, бросьте их в 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)
    

    Вскоре вы сможете сделать это мысленно без усилий.

  • Сделайте либеральное использование (doc). Невозможно переоценить полезность наличия документации, доступной прямо на REPL. Если вы используете clojure.contrib.repl-utils и имеете свои .clj файлы в пути к классам, вы можете сделать (source some-function) и просмотреть весь исходный код для него. Вы можете сделать (show some-java-class) и посмотреть описание всех методов в нем. И так далее.

Возможность читать что-то быстро приходит только с опытом. Lisp не сложнее читать, чем любой другой язык. Так получилось, что большинство языков выглядят как C, и большинство программистов проводят большую часть времени, читая это, поэтому кажется, что синтаксис Си легче читать. Практика практической практики.

Ответ 3

Сначала помните, что функциональная программа состоит из выражений, а не операторов. Например, форма (if condition expr1 expr2) принимает свой 1-й аргумент как условие для проверки логического значения, оценивает его, и если оно eval'ed к true, то оно вычисляет и возвращает expr1, в противном случае оценивает и возвращает expr2. Когда каждая форма возвращает выражение, некоторые обычные синтаксические конструкции, такие как THEN или ELSE, могут просто исчезнуть. Обратите внимание, что здесь if сам также оценивает выражение.

Теперь об оценке: В Clojure (и других Lisps) большинство форм, с которыми вы сталкиваетесь, являются вызовами функций формы (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, особенно. если у них есть хороший отступ, как в примере, который вы дали.

Надеюсь, что это поможет.