Chiamando il clojure di Java


165

La maggior parte dei principali risultati di Google per "chiamare clojure da java" sono obsoleti e si consiglia di utilizzare clojure.lang.RTper compilare il codice sorgente. Potresti aiutare con una chiara spiegazione di come chiamare Clojure da Java supponendo che tu abbia già costruito un vaso dal progetto Clojure e incluso nel percorso di classe?


8
Non so che la compilazione della fonte ogni volta sia "obsoleta" in quanto tale. È una decisione di progettazione. Lo sto facendo ora, in quanto semplifica l'integrazione del codice Clojure in un progetto Java Netbeans legacy. Aggiungi Clojure come libreria, aggiungi un file sorgente Clojure, imposta le chiamate e il supporto Clojure istantaneo senza avere più passaggi di compilazione / collegamento! Al costo di una frazione di secondo di ritardo all'avvio di ogni app.
Brian Knoblauch,


2
Vedi le ultime novità per Clojure 1.8.0 - Clojure ora ha il collegamento diretto al compilatore.
TWR Cole,

Risposte:


167

Aggiornamento : da quando è stata pubblicata questa risposta, alcuni degli strumenti disponibili sono cambiati. Dopo la risposta originale, c'è un aggiornamento che include informazioni su come creare l'esempio con gli strumenti attuali.

Non è così semplice come compilare in un barattolo e chiamare i metodi interni. Sembra che ci siano alcuni trucchi per farlo funzionare comunque. Ecco un esempio di un semplice file Clojure che può essere compilato in un vaso:

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

Se lo esegui, dovresti vedere qualcosa di simile:

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

Ed ecco un programma Java che chiama la -binomialfunzione in 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));
    }
}

Il suo output è:

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

Il primo pezzo di magia sta usando la :methodsparola chiave gen-classnell'istruzione. Ciò sembra essere necessario per consentire l'accesso alla funzione Clojure in modo simile ai metodi statici in Java.

La seconda cosa è creare una funzione wrapper che può essere chiamata da Java. Si noti che la seconda versione di -binomialha un trattino di fronte.

E ovviamente il vaso Clojure stesso deve essere sul percorso di classe. In questo esempio è stato utilizzato il vaso Clojure-1.1.0.

Aggiornamento : questa risposta è stata nuovamente testata utilizzando i seguenti strumenti:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • Aggiornamento 25.0 JDK 1.7.0

La parte di Clojure

Per prima cosa crea un progetto e la struttura di directory associata usando Leiningen:

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

Ora passa alla directory del progetto.

C:\projects>cd com.domain.tiny

Nella directory del progetto, apri il project.cljfile e modificalo in modo che il contenuto sia come mostrato di seguito.

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

Ora, assicurati che tutte le dipendenze (Clojure) siano disponibili.

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

A questo punto potresti vedere un messaggio sul download del vaso Clojure.

Ora modifica il file Clojure in modo C:\projects\com.domain.tiny\src\com\domain\tiny.cljtale che contenga il programma Clojure mostrato nella risposta originale. (Questo file è stato creato quando Leiningen ha creato il progetto.)

Gran parte della magia qui è nella dichiarazione dello spazio dei nomi. Il :gen-classdice al sistema di creare una classe denominata com.domain.tinycon un metodo statico singolo chiamato binomial, una funzione di prendere due argomenti interi e restituisce un doppio. Esistono due funzioni con binomialnomi simili , una funzione Clojure tradizionale -binomiale un wrapper accessibili da Java. Nota il trattino nel nome della funzione -binomial. Il prefisso predefinito è un trattino, ma può essere cambiato in qualcos'altro se lo si desidera. La -mainfunzione effettua solo un paio di chiamate alla funzione binomiale per assicurare che stiamo ottenendo i risultati corretti. Per fare ciò, compila la classe ed esegui il programma.

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

Dovresti vedere l'output mostrato nella risposta originale.

Ora impacchettalo in un barattolo e mettilo in un posto comodo. Copia lì anche il barattolo Clojure.

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 Java

Leiningen ha un'attività integrata lein-javac, che dovrebbe essere in grado di aiutare con la compilazione Java. Sfortunatamente, sembra essere rotto nella versione 2.1.3. Non riesce a trovare il JDK installato e non trova il repository Maven. I percorsi per entrambi hanno spazi incorporati sul mio sistema. Presumo che sia questo il problema. Qualsiasi IDE Java potrebbe gestire anche la compilazione e il packaging. Ma per questo post, andiamo alla vecchia scuola e lo facciamo dalla riga di comando.

Innanzitutto crea il file Main.javacon i contenuti mostrati nella risposta originale.

Per compilare la parte Java

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

Ora crea un file con alcune meta-informazioni da aggiungere al vaso che vogliamo costruire. In Manifest.txt, aggiungi il seguente testo

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

Ora impacchettalo in un unico grande file jar, incluso il nostro programma Clojure e il vaso 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

Per eseguire il programma:

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

L'output è essenzialmente identico a quello prodotto da Clojure da solo, ma il risultato è stato convertito in un doppio Java.

Come accennato, un IDE Java si occuperà probabilmente degli argomenti di compilazione disordinati e del packaging.


4
1. posso mettere il tuo esempio su clojuredocs.org come esempio per la macro "ns"? 2. che cosa è # ^ davanti a ": method [# ^ {: static true} [binomial [int int] double]]" (sono un principiante)?
Belun

5
@Belun, sicuro che puoi usarlo come esempio - Sono lusingato. "# ^ {: Static true}" allega alcuni metadati alla funzione indicando che il binomio è una funzione statica. In questo caso è necessario perché, dal lato Java, stiamo chiamando la funzione da main - una funzione statica. Se il binomio non fosse statico, la compilazione della funzione principale sul lato Java produrrebbe un messaggio di errore "Il binomio del metodo non statico (int, int) non può essere referenziato da un contesto statico". Ci sono ulteriori esempi sul sito Mentor oggetto.
clartaq,

4
C'è una cosa cruciale non menzionata qui: per compilare il file Clojure in classe Java, è necessario: (compilare 'com.domain.tiny)
Domchi,

come stai compilando la fonte Clojure? Questo è dove sono bloccato.
Matthew Boston,

@MatthewBoston Al momento in cui è stata scritta la risposta, ho usato Enclojure, un plugin per l'IDE NetBeans. Ora, probabilmente userei Leiningen, ma non l'ho provato o testato.
clartaq

119

A partire da Clojure 1.6.0, esiste un nuovo modo preferito per caricare e richiamare le funzioni Clojure. Questo metodo è ora preferito per chiamare direttamente RT (e sostituisce molte delle altre risposte qui). Il javadoc è qui - il punto di ingresso principale è clojure.java.api.Clojure.

Per cercare e chiamare una funzione Clojure:

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

Le funzioni in clojure.corevengono caricate automaticamente. Altri spazi dei nomi possono essere caricati tramite richiedono:

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

IFns può essere passato a funzioni di ordine superiore, ad esempio l'esempio seguente passa plus a read:

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

La maggior parte dei messaggi IFnin Clojure si riferiscono a funzioni. Alcuni, tuttavia, fanno riferimento a valori di dati non funzionali. Per accedervi, utilizzare derefinvece di fn:

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

A volte (se si utilizza un'altra parte del runtime Clojure), potrebbe essere necessario assicurarsi che il runtime Clojure sia inizializzato correttamente; per questo motivo è sufficiente chiamare un metodo sulla classe Clojure. Se non hai bisogno di chiamare un metodo su Clojure, è sufficiente semplicemente caricare la classe (in passato c'era una raccomandazione simile per caricare la classe RT; ora è preferibile):

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

1
Usando questo approccio, lo script non può usare quote '() e richiedere cose. C'è una soluzione per questo?
Renato

2
Penso che ci siano solo un paio di forme speciali che non esistono anche come var. Una soluzione alternativa è accedervi tramite Clojure.read("'(1 2 3"). Sarebbe ragionevole archiviarlo come una richiesta di miglioramento sebbene fornisca un Clojure.quote () o facendolo funzionare come var.
Alex Miller,

1
@Renato Non è necessario citare nulla, perché nulla viene valutato dalle regole di valutazione di Clojure. Se vuoi un elenco contenente i numeri 1-3, invece di scrivere '(1 2 3)scrivi qualcosa di simile Clojure.var("clojure.core", "list").invoke(1,2,3). E la risposta ha già un esempio di come usare require: è solo una variante come qualsiasi altra.
amalloy,

34

MODIFICA Questa risposta è stata scritta nel 2010 e ha funzionato in quel momento. Vedi la risposta di Alex Miller per una soluzione più moderna.

Che tipo di codice chiama da Java? Se hai generato una classe con gen-class, chiamala semplicemente. Se si desidera chiamare la funzione dallo script, guardare al seguente esempio .

Se si desidera valutare il codice dalla stringa, all'interno di Java, è possibile utilizzare il seguente codice:

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
Per espandere un po 'se si desidera accedere a def'd var nello spazio dei nomi (cioè (def my-var 10)) usare questo RT.var("namespace", "my-var").deref().
Ivan Koblik,

Non funziona per me senza aggiungere RT.load("clojure/core");all'inizio. Quale strano comportamento?
hsestupin,

Funziona ed è simile a quello che ho usato; non sono sicuro se è la tecnica più recente. Potrei usare Clojure 1.4 o 1.6, non sono sicuro.
Yan King Yin,

12

EDIT: ho scritto questa risposta quasi tre anni fa. In Clojure 1.6 esiste un'API corretta esattamente allo scopo di chiamare Clojure da Java. Per favore, la risposta di Alex Miller per informazioni aggiornate.

Risposta originale del 2011:

A mio avviso, il modo più semplice (se non si genera una classe con la compilazione AOT) è utilizzare clojure.lang.RT per accedere alle funzioni in clojure. Con esso puoi imitare ciò che avresti fatto in Clojure (non è necessario compilare le cose in modi speciali):

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

E in 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);

È un po 'più dettagliato in Java, ma spero sia chiaro che i pezzi di codice sono equivalenti.

Questo dovrebbe funzionare fino a quando Clojure e i file sorgente (o file compilati) del codice Clojure si trovano sul percorso di classe.


1
Questo consiglio non è aggiornato a partire da Clojure 1.6 - utilizzare invece clojure.java.api.Clojure.
Alex Miller,

Non una cattiva risposta al momento, ma intendevo bounty stackoverflow.com/a/23555959/1756702 come risposta autorevole per Clojure 1.6. Ci riproverò domani ...
A. Webb,

La risposta di Alex merita davvero la generosità! Per favore fatemi sapere se posso aiutare con il trasferimento della taglia se necessario.
raek,

@raek Non mi dispiace che tu abbia ricevuto un piccolo bonus dal mio grilletto sovra caffeinato. Spero di vederti di nuovo intorno al tag Clojure.
A. Webb,

10

Sono d'accordo con la risposta di clartaq, ma ho pensato che i principianti potevano anche usare:

  • informazioni dettagliate su come farlo funzionare
  • informazioni aggiornate per Clojure 1.3 e versioni recenti di leiningen.
  • un vaso Clojure che include anche una funzione principale, quindi può essere eseguito autonomamente o collegato come libreria.

Quindi ho trattato tutto ciò in questo post sul blog .

Il codice Clojure è simile al seguente:

(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 configurazione del progetto leiningen 1.7.1 è simile alla seguente:

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

Il codice Java è simile al seguente:

import ThingOne.*;

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

Oppure puoi anche ottenere tutto il codice da questo progetto su github .


Perché hai usato l'AOT? Il programma non è più indipendente dalla piattaforma?
Edward,

3

Funziona 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

Se il caso d'uso deve includere un JAR creato con Clojure in un'applicazione Java, ho trovato utile avere uno spazio dei nomi separato per l'interfaccia tra i due mondi:

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

Lo spazio dei nomi principale può utilizzare l'istanza iniettata per eseguire le sue attività:

(ns example-app.core)

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

A scopo di test, l'interfaccia può essere testata:

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

0

Un'altra tecnica che funziona anche con altre lingue oltre a JVM è dichiarare un'interfaccia per le funzioni che si desidera chiamare e quindi utilizzare la funzione 'proxy' per creare un'istanza che le implementa.


-1

Puoi anche usare la compilazione AOT per creare file di classe che rappresentano il tuo codice clojure. Leggi la documentazione su compilation, gen-class e amici nei documenti API di Clojure per i dettagli su come eseguire questa operazione, ma in sostanza creerai una classe che chiama le funzioni clojure per ogni chiamata di metodo.

Un'altra alternativa è utilizzare il nuovo defprotocol e la funzionalità deftype, che richiederanno anche la compilazione AOT ma offriranno prestazioni migliori. Non conosco ancora i dettagli su come farlo, ma una domanda sulla mailing list probabilmente farebbe il trucco.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.