Clojure aus Java aufrufen

165

Die meisten der Top-Google-Hits für "Clojure aus Java aufrufen" sind veraltet und empfehlen die Verwendung clojure.lang.RTzum Kompilieren des Quellcodes. Könnten Sie mit einer klaren Erklärung helfen, wie Clojure von Java aus aufgerufen wird, vorausgesetzt, Sie haben bereits ein JAR aus dem Clojure-Projekt erstellt und in den Klassenpfad aufgenommen?

Arthur Ulfeldt
quelle
8
Ich weiß nicht, dass das Kompilieren der Quelle jedes Mal als solches "veraltet" ist. Es ist eine Designentscheidung. Ich mache das jetzt, da es die Integration von Clojure-Code in ein altes Java Netbeans-Projekt zum Kinderspiel macht. Fügen Sie Clojure als Bibliothek hinzu, fügen Sie eine Clojure-Quelldatei hinzu, richten Sie die Aufrufe ein und unterstützen Sie Clojure sofort, ohne mehrere Kompilierungs- / Verknüpfungsschritte durchzuführen! Auf Kosten eines Bruchteils einer Sekunde Verzögerung bei jedem App-Start.
Brian Knoblauch
2
Siehe neueste Version für Clojure 1.8.0 - Clojure verfügt jetzt über eine direkte Compiler-Verknüpfung.
TWR Cole

Antworten:

166

Aktualisieren : Seit diese Antwort veröffentlicht wurde, haben sich einige der verfügbaren Tools geändert. Nach der ursprünglichen Antwort gibt es ein Update mit Informationen zum Erstellen des Beispiels mit aktuellen Tools.

Es ist nicht ganz so einfach wie das Kompilieren in ein Glas und das Aufrufen der internen Methoden. Es scheint jedoch ein paar Tricks zu geben, damit alles funktioniert. Hier ist ein Beispiel für eine einfache Clojure-Datei, die zu einem JAR kompiliert werden kann:

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

Wenn Sie es ausführen, sollten Sie Folgendes sehen:

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

Und hier ist ein Java-Programm, das die -binomialFunktion in der aufruft 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));
    }
}

Die Ausgabe lautet:

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

Das erste Stück Magie ist die Verwendung des :methodsSchlüsselworts in der gen-classAnweisung. Dies scheint erforderlich zu sein, damit Sie auf die Clojure-Funktion zugreifen können, ähnlich wie bei statischen Methoden in Java.

Die zweite Sache ist, eine Wrapper-Funktion zu erstellen, die von Java aufgerufen werden kann. Beachten Sie, dass die zweite Version von -binomialeinen Bindestrich vor sich hat.

Und natürlich muss sich das Clojure-Glas selbst auf dem Klassenpfad befinden. In diesem Beispiel wurde das Glas Clojure-1.1.0 verwendet.

Update : Diese Antwort wurde mit den folgenden Tools erneut getestet:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Update 25

Der Clojure-Teil

Erstellen Sie zunächst mit Leiningen ein Projekt und eine zugehörige Verzeichnisstruktur:

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

Wechseln Sie nun in das Projektverzeichnis.

C:\projects>cd com.domain.tiny

Öffnen Sie im Projektverzeichnis die project.cljDatei und bearbeiten Sie sie so, dass der Inhalt wie unten gezeigt ist.

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

Stellen Sie nun sicher, dass alle Abhängigkeiten (Clojure) verfügbar sind.

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

Möglicherweise wird an dieser Stelle eine Meldung zum Herunterladen des Clojure-Glases angezeigt.

Bearbeiten Sie nun die Clojure-Datei C:\projects\com.domain.tiny\src\com\domain\tiny.cljso, dass sie das in der ursprünglichen Antwort gezeigte Clojure-Programm enthält. (Diese Datei wurde erstellt, als Leiningen das Projekt erstellt hat.)

Ein Großteil der Magie liegt hier in der Namespace-Deklaration. Das :gen-classweist das System an, eine Klasse com.domain.tinymit einer einzelnen statischen Methode namens zu erstellen. Diese binomialFunktion verwendet zwei ganzzahlige Argumente und gibt ein Double zurück. Es gibt zwei ähnlich benannte Funktionen binomial, eine traditionelle Clojure-Funktion -binomialund einen Wrapper, auf den über Java zugegriffen werden kann. Beachten Sie den Bindestrich im Funktionsnamen -binomial. Das Standardpräfix ist ein Bindestrich, kann jedoch bei Bedarf in einen anderen geändert werden. Die -mainFunktion ruft nur ein paar Mal die Binomialfunktion auf, um sicherzustellen, dass wir die richtigen Ergebnisse erhalten. Kompilieren Sie dazu die Klasse und führen Sie das Programm aus.

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

Die Ausgabe sollte in der Originalantwort angezeigt werden.

Packen Sie es jetzt in ein Glas und stellen Sie es an einen geeigneten Ort. Kopieren Sie dort auch das Clojure-Glas.

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.

Der Java-Teil

Leiningen hat eine eingebaute Aufgabe, lein-javac die bei der Java-Kompilierung helfen soll. Leider scheint es in Version 2.1.3 kaputt zu sein. Das installierte JDK kann nicht gefunden werden, und das Maven-Repository kann nicht gefunden werden. Die Pfade zu beiden haben eingebettete Leerzeichen in meinem System. Ich nehme an, das ist das Problem. Jede Java-IDE kann auch die Kompilierung und Verpackung übernehmen. Aber für diesen Beitrag gehen wir auf die alte Schule und machen es an der Kommandozeile.

Erstellen Sie zuerst die Datei Main.javamit dem Inhalt der ursprünglichen Antwort.

Java-Teil kompilieren

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

Erstellen Sie nun eine Datei mit einigen Metainformationen, die Sie dem zu erstellenden JAR hinzufügen möchten. In Manifest.txtden folgenden Text

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

Packen Sie jetzt alles in eine große JAR-Datei, einschließlich unseres Clojure-Programms und des Clojure-Jars.

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

So führen Sie das Programm aus:

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

Die Ausgabe ist im Wesentlichen identisch mit der von Clojure allein, aber das Ergebnis wurde in ein Java-Double konvertiert.

Wie bereits erwähnt, wird sich eine Java-IDE wahrscheinlich um die chaotischen Kompilierungsargumente und die Verpackung kümmern.

clartaq
quelle
4
1. Kann ich Ihr Beispiel auf clojuredocs.org als Beispiel für das Makro "ns" veröffentlichen? 2. Was steht # ^ vor ": methoden [# ^ {: static true} [binomial [int int] double]]" (ich bin ein Neuling)?
Belun
5
@Belun, sicher können Sie es als Beispiel verwenden - ich bin geschmeichelt. Das "# ^ {: static true}" fügt der Funktion einige Metadaten hinzu, die angeben, dass Binomial eine statische Funktion ist. Dies ist in diesem Fall erforderlich, da wir auf der Java-Seite die Funktion von main aus aufrufen - eine statische Funktion. Wenn das Binomial nicht statisch wäre, würde das Kompilieren der Hauptfunktion auf der Java-Seite eine Fehlermeldung über "Nicht statische Methode Binomial (int, int) kann nicht aus einem statischen Kontext referenziert werden" erzeugen. Weitere Beispiele finden Sie auf der Object Mentor-Website.
Clartaq
4
Es gibt eine wichtige Sache, die hier nicht erwähnt wird - um die Clojure-Datei in die Java-Klasse zu kompilieren, müssen Sie: ('com.domain.tiny kompilieren)
Domchi
Wie kompiliert AOT die Clojure-Quelle? Hier stecke ich fest.
Matthew Boston
@MatthewBoston Als die Antwort geschrieben wurde, habe ich Enclojure verwendet, ein Plugin für die NetBeans-IDE. Jetzt würde ich wahrscheinlich Leiningen verwenden, habe es aber nicht ausprobiert oder getestet.
Clartaq
119

Ab Clojure 1.6.0 gibt es eine neue bevorzugte Methode zum Laden und Aufrufen von Clojure-Funktionen. Diese Methode wird jetzt dem direkten Aufruf von RT vorgezogen (und ersetzt viele der anderen Antworten hier). Der Javadoc ist hier - der Haupteinstiegspunkt istclojure.java.api.Clojure .

So suchen Sie eine Clojure-Funktion und rufen sie auf:

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

Funktionen in clojure.corewerden automatisch geladen. Andere Namespaces können über require geladen werden:

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

IFns kann zu höherer Ordnung Funktionen übergeben werden, zum Beispiel das folgende Beispiel Pässe pluszu read:

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

Die meisten IFns in Clojure beziehen sich auf Funktionen. Einige beziehen sich jedoch auf Nichtfunktionsdatenwerte. Um auf diese zuzugreifen, verwenden Sie derefanstelle von fn:

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

Manchmal (wenn Sie einen anderen Teil der Clojure-Laufzeit verwenden) müssen Sie möglicherweise sicherstellen, dass die Clojure-Laufzeit ordnungsgemäß initialisiert ist. Zu diesem Zweck reicht es aus, eine Methode für die Clojure-Klasse aufzurufen. Wenn Sie in Clojure keine Methode aufrufen müssen, reicht es aus, die Klasse einfach laden zu lassen (in der Vergangenheit gab es eine ähnliche Empfehlung zum Laden der RT-Klasse; dies wird jetzt bevorzugt):

Class.forName("clojure.java.api.Clojure") 
Alex Miller
quelle
1
Bei diesem Ansatz kann das Skript nicht quote '() verwenden und erfordert Dinge. Gibt es dafür eine Lösung?
Renato
2
Ich denke, es gibt nur ein paar spezielle Formen, die nicht auch als Vars existieren. Eine Problemumgehung besteht darin, über darauf zuzugreifen Clojure.read("'(1 2 3"). Es wäre vernünftig, dies als Erweiterungsanforderung einzureichen, obwohl Sie eine Clojure.quote () bereitstellen oder sie als var verwenden möchten.
Alex Miller
1
@ Renato Es besteht keine Notwendigkeit, etwas zu zitieren, da ohnehin nichts von Clojures Bewertungsregeln bewertet wird. Wenn Sie eine Liste mit den Nummern 1-3 möchten, schreiben '(1 2 3)Sie statt zu schreiben so etwas wie Clojure.var("clojure.core", "list").invoke(1,2,3). Und die Antwort enthält bereits ein Beispiel für die Verwendung require: Es ist nur eine Variable wie jede andere.
Amalloy
34

BEARBEITEN Diese Antwort wurde 2010 geschrieben und funktionierte zu dieser Zeit. Siehe Alex Millers Antwort für eine modernere Lösung.

Welche Art von Code wird von Java aufgerufen? Wenn Sie eine Klasse mit gen-class generiert haben, rufen Sie sie einfach auf. Wenn Sie die Funktion über ein Skript aufrufen möchten, sehen Sie sich das folgende Beispiel an .

Wenn Sie Code aus einer Zeichenfolge in Java auswerten möchten, können Sie folgenden Code verwenden:

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);
  }
}
Alex Ott
quelle
1
Verwenden Sie dies, um ein wenig zu erweitern, wenn Sie auf def'd var im Namespace zugreifen möchten (dh (def my-var 10)) RT.var("namespace", "my-var").deref().
Ivan Koblik
Es funktioniert nicht für mich, ohne RT.load("clojure/core");am Anfang hinzuzufügen . Was ist das seltsame Verhalten?
Hsestupin
Dies funktioniert und ähnelt dem, was ich verwendet habe. Ich bin mir nicht sicher, ob es die neueste Technik ist. Ich verwende möglicherweise Clojure 1.4 oder 1.6, bin mir nicht sicher.
Yan King Yin
12

EDIT: Ich habe diese Antwort vor fast drei Jahren geschrieben. In Clojure 1.6 gibt es eine geeignete API, um Clojure von Java aus aufzurufen. Bitte Alex Millers Antwort für aktuelle Informationen.

Ursprüngliche Antwort von 2011:

Aus meiner Sicht besteht der einfachste Weg (wenn Sie keine Klasse mit AOT-Kompilierung generieren) darin, mit clojure.lang.RT auf Funktionen in clojure zuzugreifen. Damit können Sie nachahmen, was Sie in Clojure getan hätten (Sie müssen die Dinge nicht auf besondere Weise kompilieren):

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

Und 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);

In Java ist es etwas ausführlicher, aber ich hoffe, es ist klar, dass die Codeteile gleichwertig sind.

Dies sollte funktionieren, solange sich Clojure und die Quelldateien (oder kompilierten Dateien) Ihres Clojure-Codes im Klassenpfad befinden.

raek
quelle
1
Dieser Rat ist ab Clojure 1.6 veraltet. Verwenden Sie stattdessen clojure.java.api.Clojure.
Alex Miller
Zu dieser Zeit keine schlechte Antwort, aber ich wollte stackoverflow.com/a/23555959/1756702 als maßgebliche Antwort für Clojure 1.6 verwenden. Werde es morgen nochmal versuchen ...
A. Webb
Alex 'Antwort verdient in der Tat das Kopfgeld! Bitte lassen Sie mich wissen, ob ich bei Bedarf bei der Übertragung des Kopfgeldes helfen kann.
Raek
@raek Es macht mir nichts aus, dass du einen kleinen Bonus von meinem überkoffeinhaltigen Abzugsfinger bekommen hast. Wir hoffen, Sie wieder am Clojure-Tag zu sehen.
A. Webb
10

Ich stimme der Antwort von clartaq zu, aber ich hatte das Gefühl, dass Anfänger auch Folgendes verwenden könnten:

  • Schritt-für-Schritt-Informationen, wie Sie dies tatsächlich zum Laufen bringen können
  • Informationen, die für Clojure 1.3 und aktuelle Versionen von Leiningen aktuell sind.
  • Ein Clojure-Glas, das auch eine Hauptfunktion enthält, sodass es eigenständig ausgeführt oder als Bibliothek verknüpft werden kann.

Also habe ich all das in diesem Blog-Beitrag behandelt .

Der Clojure-Code sieht folgendermaßen aus:

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

Das Projekt-Setup von leiningen 1.7.1 sieht folgendermaßen aus:

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

Der Java-Code sieht folgendermaßen aus:

import ThingOne.*;

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

Oder Sie können den gesamten Code aus diesem Projekt auf github abrufen .

Sandover
quelle
Warum hast du den AOT benutzt? Ist das Programm nicht mehr plattformunabhängig?
Edward
3

Dies funktioniert mit 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)"));
    }
}
KIM Taegyoon
quelle
2

Wenn der Anwendungsfall darin besteht, eine mit Clojure erstellte JAR in eine Java-Anwendung aufzunehmen, hat es sich als vorteilhaft erwiesen, einen separaten Namespace für die Schnittstelle zwischen den beiden Welten zu haben:

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

Der Kern-Namespace kann die injizierte Instanz verwenden, um seine Aufgaben auszuführen:

(ns example-app.core)

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

Zu Testzwecken kann die Schnittstelle gestoppt werden:

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

Eine andere Technik, die auch mit anderen Sprachen zusätzlich zu JVM funktioniert, besteht darin, eine Schnittstelle für Funktionen zu deklarieren, die Sie aufrufen möchten, und dann die 'Proxy'-Funktion zu verwenden, um eine Instanz zu erstellen, die sie implementiert.

Petr Gladkikh
quelle
-1

Sie können auch die AOT-Kompilierung verwenden, um Klassendateien zu erstellen, die Ihren Clojure-Code darstellen. Weitere Informationen hierzu finden Sie in der Dokumentation zu Kompilierung, Gen-Klasse und Freunden in den Clojure-API-Dokumenten. Im Wesentlichen erstellen Sie jedoch eine Klasse, die Clojure-Funktionen für jeden Methodenaufruf aufruft.

Eine andere Alternative ist die Verwendung der neuen Defprotokoll- und Deftype-Funktionalität, die ebenfalls eine AOT-Kompilierung erfordert, aber eine bessere Leistung bietet. Ich weiß noch nicht genau, wie das geht, aber eine Frage auf der Mailingliste würde wahrscheinlich den Trick machen.

rosejn
quelle