Знову пролунало запитання: я пишу код у REPL і бачу результати, але це не дуже зручно. Тож я вирішив написати невеликий вступ про те, як зручно займатися інтерактивною розробкою (також відомою як розробка з використанням REPL).

Одразу про головне: нам майже ніколи не потрібно нічого вводити в REPL. Єдиний виняток — команди з файлу user.clj, як-от (start) чи (reload), але це окрема тема. Ми ніколи не вводимо в REPL щось таке:

(defn add-two [n]
 (+ n 2))

Ба більше, ми навіть цього не вводимо в REPL:

(add-two 3)

То що ж ми робимо? Проблема в тому, що інтерактивну розробку за своєю природою важко пояснити, не показавши, тож, гадаю, скористаюся знімками екрана. Я використовуватиму Emacs CIDER як середовище розробки, але того самого можна досягти у VS Code з Calva, vim тощо. Щодо IntelliJ IDEA я не зовсім упевнений, тож тут не допоможу.

Почнімо.

Що таке структурне редагування?

Ще одна річ перед тим, як справді почати.

Головна відмінність LISP від «звичайних» мов — знаменита гомоіконічність. Тобто код записано з використанням власних структур даних мови (переважно списків). Один із наслідків цього — дуже чіткі межі між кожним виразом LISP, або s-виразом (s-exp). Дужки.

Це має дуже практичний наслідок. Коли у вас є щось таке (уявімо, що | — це курсор), редактор легко може визначити, що він стоїть одразу після s-виразу (+ 2 3) і всередині s-виразу (inc ...).

(inc (+ 2 3)|)

Редактору надзвичайно просто це з’ясувати. У Python для цього довелося б рахувати пробіли, тож це складніше завдання. У будь-якому LISP це просто: важливе лише розташування дужок.

Гаразд, тепер починається найцікавіше.

Правильний спосіб керувати REPL

Як я сказав вище, ми більше ніколи не вводитимемо текст у REPL (крім команд з user.clj). Але все одно розроблятимемо інтерактивно.

Коли маємо вираз (+ 2 3), у CIDER ми поставимо курсор одразу після нього:

(+ 2 3)|

І виконаємо команду M-x cider-eval-last-sexp RET (у VS Code з Calva це буде ctrl+alt+enter для Evaluate to cursor), і ось що отримаємо:

Це найважливіша частина. Редактор відправив s-вираз у REPL, отримав результат обчислення і показав його нам просто поруч із s-виразом. Вітаю, це третина всього, що потрібно знати, щоб почати інтерактивну розробку.

Отже, ми не вводили вираз у REPL, а обчислили s-вираз безпосередньо з файлу з кодом. Оце так.

Тепер розгляньмо реальний приклад налагодження (ну, напівреальний). Припустімо, маємо функцію:

(defn authorized? [handler]
    (fn [request]
      (if (some-authrorization-logic request)
        "You are authorized"
        "You are NOT authorized")))

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

У «звичайних» мовах для її налагодження знадобився б інструмент на кшталт Postman, постійне перезавантаження застосунку й проходження через проміжний шар. Але не кложуристам — у нас є інтерактивна розробка. Скористаймося єдиним прийомом, який ми вже вивчили: обчисленням останнього s-виразу.

Насамперед ми хочемо захопити запит, який спричиняє помилку, щоб не запускати Postman знову і знову. Це досить просто, наприклад так:

(defn authorized? [handler]
    (fn [request]
      (def request request)
      (if (some-authrorization-logic request)
        "You are authorized"
        "You are NOT authorized")))

Зверніть увагу на вставлений def, яким ми визначили глобальну змінну request. Це потужний механізм налагодження, але краще для цього використовувати інструмент на кшталт snitch. Так чи інакше, ми виконали запит один раз і тепер маємо його в поточному просторі імен. Як його перевірити?

Поставивши курсор одразу після слова request і виконавши єдину поки що відому нам команду для обчислення s-виразу перед курсором (а найменший s-вираз — це одне слово):

Ось і наш запит. Тепер можна ізолювати частину, яку ми налагоджуємо, знову використавши ту саму команду, але цього разу для s-виразу (some-authrorization-logic request):

Ага, щось не так із методом. Поекспериментуймо ще: той самий прийом, але тепер можна спростити аргумент, звівши запит до мапи з одним ключем, бо ми підозрюємо, що лише він і цікавить цю функцію:

Хм, delete теж не спрацював. Спробуймо POST:

Бінго, ми з’ясували, у чому була проблема в цьому дурнуватому прикладі, використавши один прийом інтерактивної розробки. Ми жодного разу не перезапускали застосунок, нам не довелося робити другий мережевий запит (дуже зручно, коли робиш це просто в продакшені), і все ж ми з’ясували причину проблеми.

Ось чому це називається інтерактивною розробкою, а не розробкою з використанням REPL: спробуйте повторити це в REPL.

Ще два прийоми

Отже, ми вивчили один прийом — обчислення виразу перед курсором, але для більш-менш вільної роботи потрібні ще принаймні два. Я не робитиму знімків для кожного, а просто розповім про них.

До речі, поки я це писав, помітив, що в CIDER є щонайменше 20 команд, які починаються з cider-eval-, а я використовую лише чотири чи п’ять.

Обчислити форму верхнього рівня, у якій ми перебуваємо

У CIDER це робиться командою M-x cider-eval-defun-at-point RET. Аналогічну команду VS Code, а також інші приклади й відео на цю тему можна знайти у статті eval tips на сайті Calva.

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

Обчислити весь файл

Найочевидніша команда: відредагуйте що завгодно, а потім відправте весь файл у REPL, щоб оновити стан запущеної програми. У CIDER це M-x cider-eval-buffer RET, у Calva — щось там, що можна знайти за посиланням вище.

Підсумок

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

Дуже важливо попрощатися зі звичкою жити в циклі редагування-компіляція-перезапуск і перейти до блаженного циклу Clojure: запустити програму один раз, а потім навчати її робити те, що вам потрібно.

Бонусна порада

Ці два інструменти буквально створені одне для одного. snitch дозволяє інструментувати будь-які змінні, що проходять через функцію, простою заміною одного символу, тож після хоча б одного виклику add-two змінна n буде доступна в глобальній області видимості:

;; note the asterix after defn
(defn* add-two [n]
 (+ n 2))

(add-two 5)

n ;; => 5

А виконання (reload) з clj-reload усе прибере й видалить усі змінні, інструментовані під час експериментів зі snitch, знову очистивши простори імен.