2010-09-20 8 views
16

En Clojure, dado el nombre de una clase como una cadena, necesito crear una nueva instancia de la clase. En otras palabras, ¿cómo iba a aplicar la nueva instancia-de-clase-enClojure: creación de una nueva instancia del nombre de la clase String

(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) 

Busco a una solución más elegante que

  • llamando al método Java newInstance en un constructor de la
  • clase
  • usando eval, la carga de cuerdas, ...

en la práctica, me va a utilizar en clases creadas usando defrecord. Entonces, si hay una sintaxis especial para ese escenario, estaría bastante interesado.

Respuesta

23

Hay dos buenas formas de hacerlo. Lo mejor depende de la circunstancia específica.

La primera es la reflexión:

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

Eso es como decir que (new Integer "16") ... incluyen cualquier otro argumento ctor que necesita en el vector a matriz. Esto es fácil, pero más lento en tiempo de ejecución que usar new con suficientes sugerencias de tipo.

La segunda opción es lo más rápido posible, pero un poco más complicado, y utiliza 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") 

El punto clave es el código eval que define una función anónima, como make-factory hace. Esto es lento - más lento que el ejemplo de reflexión anterior, por lo tanto, hágalo con la menor frecuencia posible, como una vez por clase. Pero una vez hecho esto, tiene una función Clojure normal que puede almacenar en algún lugar, en una var como int-factory en este ejemplo, o en un hash-map o vector, dependiendo de cómo lo va a usar. A pesar de todo, esta función de fábrica se ejecutará a la velocidad compilada completa, puede ser ingresada por HotSpot, etc. y siempre se ejecutará mucho más que que el ejemplo de reflexión.

Cuando se trata específicamente de clases generadas por deftype o defrecord, puede omitir la lista de tipos ya que esas clases siempre tienen exactamente dos ctors cada una con aries diferentes. Esto permite que algo como:

 
(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) 
+0

¡Excelente! La segunda opción es obviamente una técnica muy general. Ya lo he usado de otra manera. – chris

4

Dado que 'nuevo' es una forma especial, no estoy seguro de que pueda hacer esto sin una macro. Aquí hay una manera de hacerlo utilizando una macro:

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

Salida comentario de Michal en las limitaciones de esta macro.

+2

Tenga en cuenta que esto sólo funcionará si el 'S 'recibida por la macro es una (literal) cadena y no una expresión arbitraria que evalúa una cadena. En este último caso, no hay evitación de 'eval' o construcción de instancia reflexiva. –

+0

Gracias por señalar eso. No pensé en mencionar eso. – Rayne

+0

Me temo que s no será una cadena literal. He editado la pregunta para reflejar esto. – chris

2

En Clojure 1.3, defrecord se defn automáticamente una función de fábrica con el nombre de registro con "->" antepuesto. Del mismo modo, una variante que toma un mapa será el nombre del registro antepuesto con "mapa->".

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} 

Una macro como este debería trabajar para crear una instancia del nombre de la cadena del tipo de registro:

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