Llamar a clojure desde java


165

La mayoría de los principales éxitos de Google para "llamar a clojure desde java" están desactualizados y recomiendan usarlos clojure.lang.RTpara compilar el código fuente. ¿Podría ayudarnos con una explicación clara de cómo llamar a Clojure desde Java suponiendo que ya ha creado un jar desde el proyecto Clojure y lo ha incluido en el classpath?


8
No sé si compilar la fuente cada vez está "desactualizado" como tal. Es una decisión de diseño. Lo estoy haciendo ahora, ya que facilita la integración del código Clojure en un proyecto heredado de Java Netbeans. Agregue Clojure como biblioteca, agregue un archivo fuente de Clojure, configure las llamadas y soporte instantáneo de Clojure sin tener múltiples pasos de compilación / enlace. A costa de una fracción de segundo retraso en el inicio de cada aplicación.
Brian Knoblauch


2
Vea lo último para Clojure 1.8.0 - Clojure ahora tiene un enlace directo del compilador.
TWR Cole

Respuestas:


167

Actualización : desde que se publicó esta respuesta, algunas de las herramientas disponibles han cambiado. Después de la respuesta original, hay una actualización que incluye información sobre cómo construir el ejemplo con las herramientas actuales.

No es tan simple como compilar en un jar y llamar a los métodos internos. Sin embargo, parece haber algunos trucos para que todo funcione. Aquí hay un ejemplo de un archivo Clojure simple que se puede compilar en un jar:

(ns com.domain.tiny
  (:gen-class
    :name com.domain.tiny
    :methods [#^{:static true} [binomial [int int] double]]))

(defn binomial
  "Calculate the binomial coefficient."
  [n k]
  (let [a (inc n)]
    (loop [b 1
           c 1]
      (if (> b k)
        c
        (recur (inc b) (* (/ (- a b) b) c))))))

(defn -binomial
  "A Java-callable wrapper around the 'binomial' function."
  [n k]
  (binomial n k))

(defn -main []
  (println (str "(binomial 5 3): " (binomial 5 3)))
  (println (str "(binomial 10042 111): " (binomial 10042 111)))
)

Si lo ejecuta, debería ver algo como:

(binomial 5 3): 10
(binomial 10042 111): 49068389575068144946633777...

Y aquí hay un programa Java que llama a la -binomialfunción en el tiny.jar.

import com.domain.tiny;

public class Main {

    public static void main(String[] args) {
        System.out.println("(binomial 5 3): " + tiny.binomial(5, 3));
        System.out.println("(binomial 10042, 111): " + tiny.binomial(10042, 111));
    }
}

Su salida es:

(binomial 5 3): 10.0
(binomial 10042, 111): 4.9068389575068143E263

La primera pieza de magia es usar la :methodspalabra clave en la gen-classdeclaración. Eso parece ser necesario para permitirle acceder a la función Clojure, algo así como métodos estáticos en Java.

Lo segundo es crear una función de contenedor que Java pueda invocar. Observe que la segunda versión de -binomialtiene un guión delante.

Y, por supuesto, el frasco de Clojure debe estar en el camino de la clase. Este ejemplo utiliza el jar Clojure-1.1.0.

Actualización : esta respuesta se ha vuelto a probar con las siguientes herramientas:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Actualización 25

La parte de Clojure

Primero cree un proyecto y una estructura de directorios asociada usando Leiningen:

C:\projects>lein new com.domain.tiny

Ahora, cambie al directorio del proyecto.

C:\projects>cd com.domain.tiny

En el directorio del proyecto, abra el project.cljarchivo y edítelo de modo que el contenido sea el que se muestra a continuación.

(defproject com.domain.tiny "0.1.0-SNAPSHOT"
  :description "An example of stand alone Clojure-Java interop"
  :url "http://clarkonium.net/2013/06/java-clojure-interop-an-update/"
  :license {:name "Eclipse Public License"
  :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.5.1"]]
  :aot :all
  :main com.domain.tiny)

Ahora, asegúrese de que todas las dependencias (Clojure) estén disponibles.

C:\projects\com.domain.tiny>lein deps

Es posible que vea un mensaje sobre la descarga del jar Clojure en este momento.

Ahora edite el archivo Clojure de C:\projects\com.domain.tiny\src\com\domain\tiny.cljmodo que contenga el programa Clojure que se muestra en la respuesta original. (Este archivo se creó cuando Leiningen creó el proyecto).

Gran parte de la magia aquí está en la declaración del espacio de nombres. El :gen-classle dice al sistema que cree una clase nombrada com.domain.tinycon un único método estático llamado binomial, una función que toma dos argumentos enteros y devuelve un doble. Hay dos funciones con nombres similares binomial, una función Clojure tradicional -binomialy un contenedor accesible desde Java. Tenga en cuenta el guión en el nombre de la función -binomial. El prefijo predeterminado es un guión, pero se puede cambiar a otra cosa si lo desea. La -mainfunción solo hace un par de llamadas a la función binomial para garantizar que estamos obteniendo los resultados correctos. Para hacer eso, compila la clase y ejecuta el programa.

C:\projects\com.domain.tiny>lein run

Debería ver la salida que se muestra en la respuesta original.

Ahora empaquételo en un frasco y colóquelo en un lugar conveniente. Copie el frasco de Clojure allí también.

C:\projects\com.domain.tiny>lein jar
Created C:\projects\com.domain.tiny\target\com.domain.tiny-0.1.0-SNAPSHOT.jar
C:\projects\com.domain.tiny>mkdir \target\lib

C:\projects\com.domain.tiny>copy target\com.domain.tiny-0.1.0-SNAPSHOT.jar target\lib\
        1 file(s) copied.

C:\projects\com.domain.tiny>copy "C:<path to clojure jar>\clojure-1.5.1.jar" target\lib\
        1 file(s) copied.

La parte de Java

Leiningen tiene una tarea incorporada lein-javac, que debería poder ayudar con la compilación de Java. Desafortunadamente, parece estar roto en la versión 2.1.3. No puede encontrar el JDK instalado y no puede encontrar el repositorio de Maven. Los caminos a ambos tienen espacios incrustados en mi sistema. Supongo que ese es el problema. Cualquier IDE de Java también podría manejar la compilación y el empaquetado. Pero para esta publicación, vamos a la vieja escuela y lo hacemos en la línea de comando.

Primero cree el archivo Main.javacon el contenido que se muestra en la respuesta original.

Para compilar parte de Java

javac -g -cp target\com.domain.tiny-0.1.0-SNAPSHOT.jar -d target\src\com\domain\Main.java

Ahora cree un archivo con alguna metainformación para agregar al jar que queremos construir. En Manifest.txt, agregue el siguiente texto

Class-Path: lib\com.domain.tiny-0.1.0-SNAPSHOT.jar lib\clojure-1.5.1.jar
Main-Class: Main

Ahora empaquete todo en un gran archivo jar, incluido nuestro programa Clojure y el jar Clojure.

C:\projects\com.domain.tiny\target>jar cfm Interop.jar Manifest.txt Main.class lib\com.domain.tiny-0.1.0-SNAPSHOT.jar lib\clojure-1.5.1.jar

Para ejecutar el programa:

C:\projects\com.domain.tiny\target>java -jar Interop.jar
(binomial 5 3): 10.0
(binomial 10042, 111): 4.9068389575068143E263

La salida es esencialmente idéntica a la producida por Clojure solo, pero el resultado se ha convertido en un Java doble.

Como se mencionó, un IDE de Java probablemente se encargará de los argumentos de compilación desordenados y el empaquetado.


44
1. ¿Puedo poner su ejemplo en clojuredocs.org como ejemplo para la macro "ns"? 2. ¿Qué es # ^ delante de ": métodos [# ^ {: estático verdadero} [binomial [int int] doble]]" (soy un novato)?
Belun

55
@Belun, seguro que puedes usarlo como ejemplo, me siento halagado. El "# ^ {: static true}" adjunta algunos metadatos a la función que indica que binomial es una función estática. Se requiere en este caso porque, en el lado de Java, estamos llamando a la función desde main, una función estática. Si binomial no fuera estático, compilar la función principal en el lado de Java produciría un mensaje de error sobre "el método no estático binomial (int, int) no puede ser referenciado desde un contexto estático". Hay ejemplos adicionales en el sitio Object Mentor.
clartaq

44
Hay una cosa crucial que no se menciona aquí: para compilar el archivo Clojure en la clase Java, debe: (compilar 'com.domain.tiny)
Domchi

¿Cómo estás compilando AOT la fuente de Clojure? Aquí es donde estoy atrapado.
Matthew Boston

@MatthewBoston En el momento en que se escribió la respuesta, utilicé Enclojure, un complemento para NetBeans IDE. Ahora, probablemente usaría Leiningen, pero no lo he probado o probado.
clartaq

119

A partir de Clojure 1.6.0, existe una nueva forma preferida de cargar e invocar funciones de Clojure. Ahora se prefiere este método a llamar a RT directamente (y reemplaza muchas de las otras respuestas aquí). El javadoc está aquí , el punto de entrada principal es clojure.java.api.Clojure.

Para buscar y llamar a una función Clojure:

IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);

Las funciones en clojure.corese cargan automáticamente. Se pueden cargar otros espacios de nombres mediante require:

IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clojure.set"));

IFns se puede pasar a funciones de orden superior, por ejemplo, el siguiente ejemplo pasa plusa read:

IFn map = Clojure.var("clojure.core", "map");
IFn inc = Clojure.var("clojure.core", "inc");
map.invoke(inc, Clojure.read("[1 2 3]"));

La mayoría de los IFns en Clojure se refieren a funciones. Algunos, sin embargo, se refieren a valores de datos que no funcionan. Para acceder a estos, use en dereflugar de fn:

IFn printLength = Clojure.var("clojure.core", "*print-length*");
IFn deref = Clojure.var("clojure.core", "deref");
deref.invoke(printLength);

A veces (si usa alguna otra parte del tiempo de ejecución de Clojure), es posible que deba asegurarse de que el tiempo de ejecución de Clojure se inicialice correctamente; llamar a un método en la clase Clojure es suficiente para este propósito. Si no necesita llamar a un método en Clojure, simplemente hacer que la clase se cargue es suficiente (en el pasado ha habido una recomendación similar para cargar la clase RT; ahora se prefiere):

Class.forName("clojure.java.api.Clojure") 

1
Usando este enfoque, el script no puede usar quote '() y requiere cosas. ¿Hay una solución para eso?
Renato

2
Creo que solo hay un par de formas especiales que no existen como vars. Una solución alternativa es acceder a través de Clojure.read("'(1 2 3"). Sin embargo, sería razonable presentar esto como una solicitud de mejora para proporcionar un Clojure.quote () o haciéndolo funcionar como una var.
Alex Miller

1
@Renato No hay necesidad de citar nada, porque las reglas de evaluación de Clojure no evalúan nada. Si desea una lista que contenga los números 1-3, entonces, en lugar de escribir '(1 2 3), escriba algo como Clojure.var("clojure.core", "list").invoke(1,2,3). Y la respuesta ya tiene un ejemplo de cómo usarla require: es solo una var como cualquier otra.
amalloy

34

EDITAR Esta respuesta fue escrita en 2010 y funcionó en ese momento. Vea la respuesta de Alex Miller para una solución más moderna.

¿Qué tipo de código están llamando desde Java? Si tiene una clase generada con gen-class, simplemente llámela. Si desea llamar a la función desde el script, consulte el siguiente ejemplo .

Si desea evaluar el código de una cadena, dentro de Java, puede usar el siguiente código:

import clojure.lang.RT;
import clojure.lang.Var;
import clojure.lang.Compiler;
import java.io.StringReader;

public class Foo {
  public static void main(String[] args) throws Exception {
    // Load the Clojure script -- as a side effect this initializes the runtime.
    String str = "(ns user) (defn foo [a b]   (str a \" \" b))";

    //RT.loadResourceScript("foo.clj");
    Compiler.load(new StringReader(str));

    // Get a reference to the foo function.
    Var foo = RT.var("user", "foo");

    // Call it!
    Object result = foo.invoke("Hi", "there");
    System.out.println(result);
  }
}

1
Para expandir un poco si desea acceder a var. Def'd en el espacio de nombres (es decir (def my-var 10)) use esto RT.var("namespace", "my-var").deref().
Ivan Koblik

No funciona para mí sin agregar RT.load("clojure/core");al principio. ¿Qué comportamiento extraño?
hsestupin

Esto funciona y es similar a lo que he estado usando; No estoy seguro si es la técnica más nueva. Puedo estar usando Clojure 1.4 o 1.6, no estoy seguro.
Yan King Yin

12

EDITAR: escribí esta respuesta hace casi tres años. En Clojure 1.6 hay una API adecuada exactamente para llamar a Clojure desde Java. Por favor, la respuesta de Alex Miller para obtener información actualizada.

Respuesta original de 2011:

Según lo veo, la forma más simple (si no genera una clase con la compilación AOT) es usar clojure.lang.RT para acceder a las funciones en clojure. Con él puedes imitar lo que habrías hecho en Clojure (no es necesario compilar cosas de maneras especiales):

;; Example usage of the "bar-fn" function from the "foo.ns" namespace from Clojure
(require 'foo.ns)
(foo.ns/bar-fn 1 2 3)

Y en Java:

// Example usage of the "bar-fn" function from the "foo.ns" namespace from Java
import clojure.lang.RT;
import clojure.lang.Symbol;
...
RT.var("clojure.core", "require").invoke(Symbol.intern("foo.ns"));
RT.var("foo.ns", "bar-fn").invoke(1, 2, 3);

Es un poco más detallado en Java, pero espero que esté claro que las piezas de código son equivalentes.

Esto debería funcionar siempre que Clojure y los archivos fuente (o archivos compilados) de su código Clojure estén en el classpath.


1
Este consejo está desactualizado a partir de Clojure 1.6: utilice clojure.java.api.Clojure en su lugar.
Alex Miller

No fue una mala respuesta en ese momento, pero tenía la intención de recompensar stackoverflow.com/a/23555959/1756702 como la respuesta autorizada para Clojure 1.6. Lo intentaremos de nuevo mañana ...
A. Webb

¡La respuesta de Alex realmente merece la recompensa! Avíseme si puedo ayudar con la transferencia de la recompensa si es necesario.
raek

@raek No me importa que obtuvieras una pequeña bonificación de mi dedo en gatillo sobre cafeinado. Espero verte de nuevo alrededor de la etiqueta Clojure.
A. Webb

10

Estoy de acuerdo con la respuesta de clartaq, pero sentí que los principiantes también podrían usar:

  • información paso a paso sobre cómo hacer que esto funcione
  • información actualizada para Clojure 1.3 y versiones recientes de leiningen.
  • un jar Clojure que también incluye una función principal, por lo que puede ejecutarse de forma independiente o vincularse como una biblioteca.

Así que cubrí todo eso en esta publicación de blog .

El código Clojure se ve así:

(ns ThingOne.core
 (:gen-class
    :methods [#^{:static true} [foo [int] void]]))

(defn -foo [i] (println "Hello from Clojure. My input was " i))

(defn -main [] (println "Hello from Clojure -main." ))

La configuración del proyecto leiningen 1.7.1 se ve así:

(defproject ThingOne "1.0.0-SNAPSHOT"
  :description "Hello, Clojure"
  :dependencies [[org.clojure/clojure "1.3.0"]]
  :aot [ThingOne.core]
  :main ThingOne.core)

El código Java se ve así:

import ThingOne.*;

class HelloJava {
    public static void main(String[] args) {
        System.out.println("Hello from Java!");
        core.foo (12345);
    }
}

O también puede obtener todo el código de este proyecto en github .


¿Por qué usaste el AOT? ¿No hace que el programa ya no sea independiente de la plataforma?
Edward

3

Esto funciona con Clojure 1.5.0:

public class CljTest {
    public static Object evalClj(String a) {
        return clojure.lang.Compiler.load(new java.io.StringReader(a));
    }

    public static void main(String[] args) {
        new clojure.lang.RT(); // needed since 1.5.0        
        System.out.println(evalClj("(+ 1 2)"));
    }
}

2

Si el caso de uso es incluir un JAR construido con Clojure en una aplicación Java, he encontrado que tener un espacio de nombres separado para la interfaz entre los dos mundos es beneficioso:

(ns example-app.interop
  (:require [example-app.core :as core])

;; This example covers two-way communication: the Clojure library 
;; relies on the wrapping Java app for some functionality (through
;; an interface that the Clojure library provides and the Java app
;; implements) and the Java app calls the Clojure library to perform 
;; work. The latter case is covered by a class provided by the Clojure lib.
;; 
;; This namespace should be AOT compiled.

;; The interface that the java app can implement
(gen-interface
  :name com.example.WeatherForecast
  :methods [[getTemperature [] Double]])

;; The class that the java app instantiates
(gen-class
  :name com.example.HighTemperatureMailer
  :state state
  :init init
  ;; Dependency injection - take an instance of the previously defined
  ;; interface as a constructor argument
  :constructors {[com.example.WeatherForecast] []}
  :methods [[sendMails [] void]])

(defn -init [weather-forecast]
  [[] {:weather-forecast weather-forecast}])

;; The actual work is done in the core namespace
(defn -sendMails
  [this]
  (core/send-mails (.state this)))

El espacio de nombres principal puede usar la instancia inyectada para realizar sus tareas:

(ns example-app.core)

(defn send-mails 
  [{:keys [weather-forecast]}]
  (let [temp (.getTemperature weather-forecast)] ...)) 

Para fines de prueba, la interfaz se puede tropezar:

(example-app.core/send-mails 
  (reify com.example.WeatherForecast (getTemperature [this] ...)))

0

Otra técnica que funciona también con otros idiomas además de JVM es declarar una interfaz para las funciones que desea llamar y luego usar la función 'proxy' para crear una instancia que las implemente.


-1

También puede usar la compilación AOT para crear archivos de clase que representen su código de clojure. Lea la documentación sobre compilación, gen-class y amigos en los documentos de la API de Clojure para obtener detalles sobre cómo hacer esto, pero en esencia creará una clase que llama a las funciones de clojure para cada invocación de método.

Otra alternativa es utilizar la nueva funcionalidad defprotocol y deftype, que también requerirá compilación AOT pero proporcionará un mejor rendimiento. Todavía no conozco los detalles de cómo hacer esto, pero una pregunta en la lista de correo probablemente funcionaría.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.