Як читати подумки код Lisp / Clojure


76

Велике спасибі за всі гарні відповіді! Не можна позначити лише одну як правильну

Примітка: Вже вікі

Я новачок у функціональному програмуванні, і хоча я можу читати прості функції у функціональному програмуванні, наприклад, обчислюючи факторіал числа, мені важко читати великі функції. Частково причина полягає в тому, що я думаю через мою нездатність з'ясувати менші блоки коду в межах визначення функції, а також частково тому, що мені стає важко зіставити ( )код.

Було б чудово, якби хтось міг провести мене з читанням коду та дати кілька порад щодо швидкого розшифрування коду.

Примітка: Я можу зрозуміти цей код, якщо дивлюсь на нього протягом 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 полягає в тому, що її важко читати, тому не сподівайтесь, що вона швидко стане для вас інтуїтивною.
Nate CK

10
Це непроста функція. Якщо ви зможете це повністю зрозуміти через 10 хвилин, ви в порядку.
Michiel de Mare

Відповіді:


49

Зокрема, код 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 не сильно відрізняється.


У вашому другому зразку коду, як я можу керувати відступом п’ятого рядка, тобто (lazy-seq, в Emacs? За замовчуванням він вирівнюється з 'y' у попередньому рядку.
Вей Ху

Вибачте, я не знаю. Я не використовую clojure, і цей приклад не перекладається точно на схему. Ви можете перевірити наявність такої змінної, як clojure-indent-offset. Наприклад, для Haskell мені довелося додати '(haskell-indent-offset 2) до моїх custom-set-змінних.
Nathan Shively-Sanders

59

Я думаю concat, це поганий приклад, який потрібно спробувати зрозуміти. Це основна функція, і вона нижча за рівень коду, який ви зазвичай пишете самі, оскільки вона прагне бути ефективною.

Майте на увазі ще одне, що код Clojure надзвичайно щільний у порівнянні з кодом Java. Трохи коду Clojure робить багато роботи. Той самий код у Java не міг би складати 23 рядки. Ймовірно, це буде декілька класів та інтерфейсів, безліч методів, безліч локальних тимчасових змінних, що викидаються, та незручні циклічні конструкції та взагалі всі типи зразків.

Деякі загальні поради, хоча ...

  1. Намагайтеся більшу частину часу ігнорувати парени. Замість цього використовуйте відступ (як пропонує Натан Сандерс). напр

    (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)і побачити опис усіх методів у ньому. І так далі.

Можливість читати щось швидко приходить лише з досвідом. Лисп читати не важче, ніж будь-яку іншу мову. Так трапляється, що більшість мов виглядають як C, і більшість програмістів проводять більшу частину свого часу, читаючи це, тому, схоже, синтаксис C легше читати. Практика практика практика.


3
+1 за згадку про функцію `` джерело ''. Цей біт інформації важче отримати, ніж мав би бути. Тепер переглядаю решту clojure.contrib.repl-utils, щоб побачити, що я пропустив.
vedang

3
+1 за перекидання проміжних результатів у let, зробив код набагато зручнішим для читання (для початківця Clojure)
Грег К

Хтось дійсно повинен зробити кілька статей або навіть книгу про те, як навчитися LISP як програміст, який не є LISP (наприклад, що надходить з Python). Здається, це настільки інакше, що нам, хто не є ЛІСП, потрібно навчитися вчити ЛІСП. Після цього пояснення воно не здається настільки непроникним. Раніше я подивився на код LISP і подумав: "який сенс намагатися?".
боб

Хтось знає про такі книги, які вони можуть запропонувати?
боб

7

Спочатку пам’ятайте, що функціональна програма складається з виразів, а не висловлювань. Наприклад, форма (if condition expr1 expr2) приймає 1-й аргумент як умову для перевірки логічного значення, обчислює його, і якщо він перейшов у значення 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, особливо. якщо вони мають гарні відступи, як приклад, який ви дали.

Сподіваюся, це допомагає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.