Я пытаюсь использовать Clojure для динамичной генерации функций, которые могут быть применены к большим объемам данных - т.е. требование - то, что функции компилируются в байт-код для выполнения быстро, но их спецификация не известна до времени выполнения.
например, предположите, что я указываю функции с простым DSL как:
(def my-spec [:add [:multiply 2 :param0] 3])
Я хотел бы создать функциональную спецификацию компиляции, таким образом что:
(compile-spec my-spec)
Возвратил бы скомпилированную функцию одного параметра x, который возвращается 2x+3.
Что лучший способ состоит в том, чтобы сделать это в Clojure?
Хамза Ерликая уже отметил самое важное, что код Clojure всегда компилируется. Я просто добавляю иллюстрацию и некоторую информацию о некоторых малоизвестных фруктах для ваших усилий по оптимизации.
Во-первых, вышеупомянутый пункт о том, что код Clojure всегда компилируется, включает замыкания, возвращаемые функциями более высокого порядка, и функции, созданные путем вызова eval
в fn
/ fn *
] и все остальное, что может действовать как функция Clojure. Таким образом, вам не нужен отдельный DSL для описания функций, просто используйте функции более высокого порядка (и, возможно, макросы):
(defn make-affine-function [a b]
(fn [x] (+ (* a x) b)))
((make-affine-function 31 47) 5)
; => 202
Было бы интереснее, если бы ваши спецификации включали информацию о типах параметров, так как тогда вы могли бы быть заинтересован в написании макроса для генерации кода с использованием этих подсказок типа. Самый простой пример, который я могу придумать, - это вариант вышеизложенного:
(defmacro make-primitive-affine-function [t a b]
(let [cast #(list (symbol (name t)) %)
x (gensym "x")]
`(fn [~x] (+ (* ~(cast a) ~(cast x)) ~(cast b)))))
((make-primitive-affine-function :int 31 47) 5)
; => 202
Используйте : int
, : long
, : float
или : double
(или символы соответствующих имен без указания пространства имен) в качестве первого аргумента, чтобы воспользоваться преимуществами распакованной примитивной арифметики, подходящей для ваших типов аргументов. В зависимости от того, что делает ваша функция, это может дать вам очень значительный прирост производительности.
Другие типы подсказок обычно предоставляются с синтаксисом # ^ Foo bar
( ^ Foo bar
делает то же самое в 1.2); если вы хотите добавить их в сгенерированный макросами код, изучите функцию with-meta
(вам необходимо объединить '{: tag Foo}
в метаданные символов, представляющих формальные аргументы ваших функций или let
-введенные локальные переменные, которым вы хотите добавить подсказки типа).
О, и если вы все еще хотите знать, как реализовать свою первоначальную идею ...
Вы всегда можете построить выражение Clojure для определения вашей функции - (list 'fn [' x ] (a-magic-function-to-generate-some-code some-args ...))
- и вызовите eval
для получения результата. Это позволит вам сделать что-то вроде следующего (было бы проще потребовать, чтобы спецификация включала список параметров, но здесь версия, предполагающая, что аргументы должны быть извлечены из спецификации, все они называются paramFOO
и должны быть лексикографически отсортированы):
(require '[clojure.walk :as walk])
(defn compile-spec [spec]
(let [params (atom #{})]
(walk/prewalk
(fn [item]
(if (and (symbol? item) (.startsWith (name item) "param"))
(do (swap! params conj item)
item)
item))
spec)
(eval `(fn [~@(sort @params)] ~@spec))))
(def my-spec '[(+ (* 31 param0) 47)])
((compile-spec my-spec) 5)
; => 202
В подавляющем большинстве случаев нет веских причин делать что-то таким образом, и этого следует избегать; вместо этого используйте функции и макросы высшего порядка. Однако, если вы занимаетесь чем-то вроде эволюционного программирования, то оно есть, обеспечивая максимальную гибкость - и в результате все равно остается скомпилированная функция.
Даже если вы не компилируете свой код AOT, как только вы определяете функцию, она компилируется в байт-код на лету.