Хорошие примеры использования макросов Clojure, которые демонстрируют преимущества языка над мейнстримом?
Я думаю об обучении Clojure, но исходя из основанного на c-синтаксисе (java, php, С#) мира императивных языков, который будет проблемой, поэтому, естественно, спрашивает себя, действительно ли это стоит? И хотя такое утверждение вопроса может быть очень субъективным и трудным для управления, есть одна особенность Clojure (и, в более общем смысле, lisps), о которой я продолжаю читать, которая должна сделать ее самым гибким языком когда-либо: макросы.
Есть ли у вас хорошие примеры использования макросов в Clojure, для целей, которые в других основных языках (рассмотрите любые из С++, PHP, Perl, Python, Groovy/Java, С#, JavaScript) потребует гораздо меньше элегантные решения/много ненужных абстракций/хаков/и т.д.
Ответы
Ответ 1
в базе clojure есть много макросов, о которых вы не думаете... Что является признаком хорошего макроса, они позволяют вам расширять язык способами, облегчающими жизнь. Без макросов жизнь была бы намного менее захватывающей. например, если у нас не было
(with-out-str (somebody else code that prints to screen))
тогда вам нужно будет изменить свой код так, чтобы у вас не было доступа.
еще один отличный пример:
(with-open-file [fh (open-a-file-code ...)]
(do (stuff assured that the file won't leak)))
весь макрос with-something-do
макросов действительно добавлен в экосистему clojure.
другая сторона пресловутой макросов заключается в том, что я трачу практически все мое текущее профессиональное время clojure, используя очень большую тяжелую библиотеку, и поэтому я трачу много времени на то, что макросы не составляют хорошо и не являются первоклассными. Авторы этой библиотеки собираются в большой версии в следующей версии, чтобы сделать все доступные функции без прохождения макросов, чтобы люди вроде меня использовали их в более высоких функциях порядка, таких как map
и reduce
.
Макросы улучшают мир, когда они облегчают жизнь. Они могут иметь обратный эффект, когда они являются единственным интерфейсом для библиотеки. , пожалуйста, не используйте макросы в качестве интерфейсов
В целом трудно получить правильную форму ваших данных. Если в качестве автора библиотеки у вас хорошо структурированы данные о том, как вы предполагаете, что используете вашу библиотеку, вполне возможно, что существует способ переструктурировать вещи, чтобы пользователи могли использовать вашу библиотеку новыми и невообразимыми способами. В этом случае структура фантастической библиотеки, о которой идет речь, была действительно неплохой, она допускала то, что авторы не предполагали. К несчастью, большая библиотека была ограничена, потому что интерфейс был набором макросов, а не набором функций. библиотека была лучше, чем макросы, поэтому они вернули ее. Это не означает, что макросы в чем-то виноваты, просто программирование сложно, и это еще один инструмент, который может иметь много эффектов, и все части должны использоваться вместе, чтобы хорошо работать.
Ответ 2
Я нахожу макросы довольно полезными для определения новых языковых функций. В большинстве языков вам нужно будет дождаться появления новой версии языка для получения нового синтаксиса - в Lisp вы можете просто расширить базовый язык с помощью макросов и добавить функции самостоятельно.
Например, Clojure не имеет обязательного цикла C
(defmacro for-loop [[sym init check change :as params] & steps]
(cond
(not (vector? params))
(throw (Error. "Binding form must be a vector for for-loop"))
(not= 4 (count params))
(throw (Error. "Binding form must have exactly 4 arguments in for-loop"))
:default
`(loop [~sym ~init value# nil]
(if ~check
(let [new-value# (do [email protected])]
(recur ~change new-value#))
value#))))
Использование следующим образом:
(for-loop [i 0, (< i 10), (inc i)]
(println i))
Хорошая идея добавить императивный цикл к функциональному языку - это дискуссия, которую мы, вероятно, должны избегать здесь: -)
Ответ 3
Там также есть более эзотерический прецедент для макросов, которые я иногда использую: написание сжатого, читаемого кода, который также полностью оптимизирован. Здесь тривиальный пример:
(defmacro str* [& ss] (apply str (map eval ss)))
То, что это делает, является конкатенацией строк во время компиляции (конечно, они должны быть константами времени компиляции). Регулярная функция конкатенации строк в Clojure равна str
, поэтому везде, где в коде с ограниченным циклом у меня есть длинная строка, которую я хотел бы разбить на несколько строковых литералов, я просто добавляю звезду к str
и изменяю время выполнения конкатенация к времени компиляции. Использование:
(str* "I want to write some very lenghty string, most often it will be a complex"
" SQL query. I'd hate if it meant allocating the string all over every time"
" this is executed.")
Другой, менее тривиальный пример:
(defmacro jprint [& xs] `(doto *out* [email protected](for [x xs] `(.append ~x))))
&
означает, что он принимает переменное количество аргументов (varargs, variadic function). В Clojure вызов вариационной функции использует выделенную кучей коллекцию для передачи аргументов (например, в Java, которая использует массив). Это не очень оптимально, но если я использую макрос, как указано выше, то нет вызова функции. Я использую его следующим образом:
(jprint \" (json-escape item) \")
Он компилируется в три вызова PrintWriter.append
(в основном развернутый цикл).
Наконец, я хотел бы показать вам еще более радикально другое. Вы можете использовать макрос, чтобы помочь вам в определении класса похожих функций, исключая количество обширных шаблонов. Возьмем этот знакомый пример: в HTTP-клиентской библиотеке нам нужна отдельная функция для каждого из методов HTTP. Каждое определение функции довольно сложно, так как оно имеет четыре перегруженных подписи. Кроме того, каждая функция включает в себя другой класс запроса из библиотеки Apache HttpClient, но все остальное точно для всех HTTP-методов. Посмотрите, сколько кода мне нужно для этого.
(defmacro- def-http-method [name]
`(defn ~name
([~'url ~'headers ~'opts ~'body]
(handle (~(symbol (str "Http" (s/capitalize name) ".")) ~'url) ~'headers ~'opts ~'body))
([~'url ~'headers ~'opts] (~name ~'url ~'headers ~'opts nil))
([~'url ~'headers] (~name ~'url ~'headers nil nil))
([~'url] (~name ~'url nil nil nil))))
(doseq [m ['GET 'POST 'PUT 'DELETE 'OPTIONS 'HEAD]]
(eval `(def-http-method ~m)))
Ответ 4
Простым примером полезного макроса, который довольно сложно воссоздать без макросов, является doto
. Он оценивает свой первый аргумент и затем оценивает следующие формы, вставляя результат оценки в качестве своего первого аргумента. Это может показаться не очень похожим, но...
С doto
это:
(let [tmpObject (produceObject)]
(do
(.setBackground tmpObject GREEN)
(.setThis tmpObject foo)
(.setThat tmpObject bar)
(.outputTo tmpObject objectSink)))
Становится следующим:
(doto (produceObject)
(.setBackground GREEN)
(.setThis foo)
(.setThat bar)
(.outputTo objectSink))
Важно то, что doto
не является магии - вы можете (перестроить) самостоятельно, используя стандартные функции языка.
Ответ 5
Какой-то фактический код из проекта, над которым я работал - мне нужны вложенные петли for-esque, используя unboxed ints.
(defmacro dofor
[[i begin end step & rest] & body]
(when step
`(let [end# (long ~end)
step# (long ~step)
comp# (if (< step# 0)
>
<)]
(loop [~i ~begin]
(when (comp# ~i end#)
[email protected](if rest
`((dofor ~rest [email protected]))
body)
(recur (unchecked-add ~i step#)))))))
Используется как
(dofor [i 2 6 2
j i 6 1]
(println i j))
Что печатает
2 2
2 3
2 4
2 5
4 4
4 5
Он компилируется в нечто очень близкое к необработанному loop
/recur
, который я изначально пишу вручную, поэтому в целом нет никакого штрафа за производительность исполнения, в отличие от эквивалентного
(doseq [i (range 2 6 2)
j (range i 6 1)]
(println i j))
Я думаю, что полученный код довольно выгодно сравнивается с эквивалентом java:
for (int i = 2; i < 6; i+=2) {
for (int j = i; j < 6; j++) {
System.out.println(i+" "+j);
}
}
Ответ 6
Макросы являются частью Clojure, но IMHO не верят, что именно поэтому вам следует или не следует изучать Clojure. Неизбежность данных, хорошие конструкции для обработки параллельного состояния, а также то, что это язык JVM и может использовать Java-код, - три причины. Если вы не можете найти другую причину для изучения Clojure, рассмотрите тот факт, что язык функционального программирования, вероятно, должен положительно повлиять на то, как вы приближаетесь к проблемам на любом языке.
Чтобы посмотреть на макросы, я предлагаю вам начать с Clojure макросов потоковой передачи: thread-first и thread-last ->
и ->>
соответственно; посетите эту страницу, и многие из различных блогов, которые обсуждают Clojure.
Удачи и получайте удовольствие.