Статьи

Осторожнее с def в Clojure

Давайте начнем с головоломки. Давайте создадим маленький проект Leiningen, который называется осторожным. Давайте установим: maincare.core в project.clj и поместим это в care / core.clj:

(ns careful.core
  (:gen-class))
 
(defn get-my-value []
  (println "Sleeping...")
  (Thread/sleep 5000)
  (println "Woke up")
  "Done")
 
(def my-def (get-my-value))
 
(defn -main [& args]
  (println "Hello, World!"))

Вот вопрос: что происходит, когда вы компилируете этот проект?

И ответ …

$ time lein2 compile
Compiling careful.core
Sleeping...
Woke up
Compilation succeeded.

real	0m7.098s
user	0m5.276s
sys	0m0.212s

Эй, моя программа напечатала что-то во время компиляции! И это заняло слишком много времени. Я этого не ожидал.

Такая явно безобидная защита может доставить вам массу неприятностей. Конечно, никто не спит так, но как насчет:

  • Код, который вычисляет что-то, возможно что-то, занимающее время или пространство?
  • Код, который загружает что-то из сети?

Я еще не понимаю, почему это решается во время компиляции.

Но я могу понять, почему использование def таким способом не очень хорошая идея. Это старая императивная привычка. Это может быть совершенно действительная императивная программа:

public static void main(String[] args) {
    Data data = loadDataFromInternet();
    ProcessedData proc = process(data);
    generateReport(proc);
}

У вас может возникнуть соблазн сделать это в Clojure следующим образом:

(def data (load-data-from-internet))
(def proc (process data))
(defn -main [& args]
  (generate-report proc))

… но это по-прежнему императивный стиль, и он кажется неправильным.

Это также настоящая боль, чтобы проверить.

Как насчет одного из этих эквивалентов?

defn -main [& args]
  (let [data (load-data-from-internet)]
     (generate-report (process proc))))
(defn -main [& args]
   (generate-report (process (load-data-from-internet))))

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

Обновить

Как отметил djork в Reddit , это потому, что def создает переменную в текущем пространстве имен с определенным значением.

Это имеет некоторый смысл, когда вы думаете о том, как выглядит defn — это действительно макрос-оболочка def (также указанная djork). И мы ожидаем, что функции, представленные defn, будут скомпилированы, верно. Даже в документах четко указано, что defn — это то же самое, что и (def name (fn [params *] exprs *)) «.

Я все еще нахожу это очень запутанным, хотя. Интересно, я просто злоупотребляю языком.

Второе обновление

Я вернулся к этому позже, и, возможно, наконец понял .

Это совершенно правильное утверждение:

(def my-def (get-my-value))

Но как насчет этого?

; Unexpected argument:
(def my-def (get-my-value))
 
; Type cast exception (vector to number)
(def my-def-2 (+ 2 []))
 
; Whatever invalid statement
(def my-def-2 (+ 2 +))

Должны ли они генерировать ошибку во время компиляции? Это имеет смысл, верно?

Теперь, когда вы печатаете это:

(def my-def (get-current-date))

Ожидаете ли вы, что во время выполнения он будет иметь состояние во время компиляции или во время выполнения? Другими словами, это должна быть дата компиляции или «сейчас» во время исполнения? Последний, верно?

Я понимаю, почему необходимы обе оценки (во время компиляции и во время выполнения). В зависимости от точки зрения, это либо какая-то хрупкость языка, либо разработчик злоупотребляет языком. В любом случае, вывод остается прежним: осторожнее с этим защитом, Юджин.

обсуждение

Помимо этого блога, есть интересная дискуссия с более подробной информацией в Reddit . Спасибо ребята!