Подтвердить что ты не робот

Clojure: создание нового экземпляра из имени класса String

В Clojure, учитывая имя класса в виде строки, мне нужно создать новый экземпляр класса. Другими словами, как я мог бы реализовать new-instance-from-class-name в

(def my-class-name "org.myorg.pkg.Foo")
; calls constructor of org.myorg.pkg.Foo with arguments 1, 2 and 3
(new-instance-from-class-name  my-class-name 1 2 3) 

Я ищу решение, более элегантное, чем

  • вызов метода Java newInstance для конструктора из класса
  • с использованием eval, load-string,...

На практике я буду использовать его для классов, созданных с помощью defrecord. Поэтому, если для этого сценария есть какой-то специальный синтаксис, мне было бы очень интересно.

4b9b3361

Ответ 1

Есть два хороших способа сделать это. Это лучше всего зависит от конкретного обстоятельства.

Первое - это отражение:

(clojure.lang.Reflector/invokeConstructor
  (resolve (symbol "Integer"))
  (to-array ["16"]))

То, что вызов (new Integer "16")... включает любые другие аргументы ctor, которые вам нужны в векторе to-array. Это легко, но медленнее во время выполнения, чем при использовании new с достаточными типами подсказок.

Вторая опция работает как можно быстрее, но немного сложнее и использует eval:

(defn make-factory [classname & types]
  (let [args (map #(with-meta (symbol (str "x" %2)) {:tag %1}) types (range))]
    (eval `(fn [[email protected]] (new ~(symbol classname) [email protected])))))

(def int-factory (make-factory "Integer" 'String))

(int-factory "42")

Ключевым моментом является код eval, который определяет анонимную функцию, как это делает make-factory. Это медленный - медленнее, чем пример отражения выше, поэтому делайте это как можно реже, например, один раз в классе. Но при этом у вас есть регулярная функция Clojure, которую вы можете хранить где-нибудь, в var, например int-factory в этом примере, или в хэш-карте или векторе в зависимости от того, как вы будете ее использовать. Независимо от того, эта функция factory будет работать с полной скомпилированной скоростью, может быть встроена в HotSpot и т.д. И всегда будет работать намного быстрее быстрее.

Когда вы конкретно занимаетесь классами, сгенерированными deftype или defrecord, вы можете пропустить список типов, поскольку эти классы всегда имеют ровно два ctors каждый с разными уровнями. Это позволяет что-то вроде:

(defn record-factory [recordname]
  (let [recordclass ^Class (resolve (symbol recordname))
        max-arg-count (apply max (map #(count (.getParameterTypes %))
                                      (.getConstructors recordclass)))
        args (map #(symbol (str "x" %)) (range (- max-arg-count 2)))]
    (eval `(fn [[email protected]] (new ~(symbol recordname) [email protected])))))


(defrecord ExampleRecord [a b c])

(def example-record-factory (record-factory "ExampleRecord"))

(example-record-factory "F." "Scott" 'Fitzgerald)

Ответ 2

Поскольку "новый" является специальной формой, я не уверен, что вы можете сделать это без макроса. Вот как это сделать, используя макрос:

user=> (defmacro str-new [s & args] `(new ~(symbol s) [email protected]))
#'user/str-new
user=> (str-new "String" "LOL")
"LOL"

Отметьте комментарий Михала об ограничениях этого макроса.

Ответ 3

Ниже приведен метод расширения defrecord для автоматического создания хорошо известных функций конструктора для создания экземпляров записи (либо новых, либо на основе существующей записи).

http://david-mcneil.com/post/765563763/enhanced-clojure-records

Ответ 4

В Clojure 1.3 defrecord автоматически отключит функцию factory, используя имя записи с добавлением "- > ". Аналогичным образом, вариант, который берет карту, будет именем записи, добавленным с помощью "map → ".

user=> (defrecord MyRec [a b])
user.MyRec
user=> (->MyRec 1 "one")
#user.MyRec{:a 1, :b "one"}
user=> (map->MyRec {:a 2})
#user.MyRec{:a 2, :b nil}

Подобный макрос должен работать для создания экземпляра из имени строки типа записи:

(defmacro newbie [recname & args] `(~(symbol (str "->" recname)) [email protected]))