Appeler clojure depuis Java


165

La plupart des meilleurs hits Google pour "appeler clojure depuis Java" sont obsolètes et recommandent de les utiliser clojure.lang.RTpour compiler le code source. Pourriez-vous aider avec une explication claire de la façon d'appeler Clojure à partir de Java en supposant que vous avez déjà construit un fichier jar à partir du projet Clojure et que vous l'avez inclus dans le classpath?


8
Je ne sais pas que la compilation de la source à chaque fois est «dépassée» en tant que telle. C'est une décision de conception. Je le fais maintenant, car cela facilite l'intégration du code Clojure dans un ancien projet Java Netbeans. Ajoutez Clojure en tant que bibliothèque, ajoutez un fichier source Clojure, configurez les appels et le support instantané de Clojure sans avoir plusieurs étapes de compilation / liaison! Au prix d'une fraction de seconde de retard à chaque démarrage d'application.
Brian Knoblauch


2
Voir la dernière version de Clojure 1.8.0 - Clojure dispose désormais d'un lien direct avec le compilateur.
TWR Cole

Réponses:


167

Mise à jour : Depuis que cette réponse a été publiée, certains des outils disponibles ont changé. Après la réponse d'origine, il y a une mise à jour comprenant des informations sur la façon de construire l'exemple avec les outils actuels.

Ce n'est pas aussi simple que de compiler dans un fichier jar et d'appeler les méthodes internes. Il semble cependant y avoir quelques astuces pour que tout fonctionne. Voici un exemple de fichier Clojure simple qui peut être compilé dans un fichier 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 vous l'exécutez, vous devriez voir quelque chose comme:

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

Et voici un programme Java qui appelle la -binomialfonction dans le 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));
    }
}

Sa sortie est:

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

Le premier élément de magie consiste à utiliser le :methodsmot - clé dans la gen-classdéclaration. Cela semble nécessaire pour vous permettre d'accéder à la fonction Clojure quelque chose comme des méthodes statiques en Java.

La deuxième chose est de créer une fonction wrapper qui peut être appelée par Java. Notez que la deuxième version de -binomiala un tiret devant.

Et bien sûr, le pot Clojure lui-même doit être sur le chemin des classes. Cet exemple utilise le jar Clojure-1.1.0.

Mise à jour : cette réponse a été retestée à l'aide des outils suivants:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 mise à jour 25

La partie Clojure

Créez d'abord un projet et la structure de répertoires associée à l'aide de Leiningen:

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

Maintenant, passez au répertoire du projet.

C:\projects>cd com.domain.tiny

Dans le répertoire du projet, ouvrez le project.cljfichier et modifiez-le de sorte que le contenu soit comme indiqué ci-dessous.

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

Maintenant, assurez-vous que toutes les dépendances (Clojure) sont disponibles.

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

Vous pouvez voir un message sur le téléchargement du bocal Clojure à ce stade.

Modifiez maintenant le fichier Clojure de C:\projects\com.domain.tiny\src\com\domain\tiny.cljsorte qu'il contienne le programme Clojure indiqué dans la réponse d'origine. (Ce fichier a été créé lorsque Leiningen a créé le projet.)

Une grande partie de la magie ici est dans la déclaration d'espace de noms. Le :gen-classdit au système de créer une classe nommée com.domain.tinyavec une seule méthode statique appelée binomial, une fonction prenant deux arguments entiers et retournant un double. Il existe deux fonctions nommées de manière similaire binomial, une fonction Clojure traditionnelle -binomialet un wrapper accessible depuis Java. Notez le trait d'union dans le nom de la fonction -binomial. Le préfixe par défaut est un trait d'union, mais il peut être changé en autre chose si vous le souhaitez. La -mainfonction fait juste quelques appels à la fonction binomiale pour s'assurer que nous obtenons les bons résultats. Pour ce faire, compilez la classe et exécutez le programme.

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

Vous devriez voir la sortie affichée dans la réponse originale.

Maintenant, emballez-le dans un bocal et placez-le dans un endroit pratique. Copiez le pot Clojure là aussi.

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 partie Java

Leiningen a une tâche intégrée lein-javac, qui devrait pouvoir aider à la compilation Java. Malheureusement, il semble être cassé dans la version 2.1.3. Il ne peut pas trouver le JDK installé et il ne peut pas trouver le référentiel Maven. Les chemins vers les deux ont des espaces intégrés sur mon système. Je suppose que c'est le problème. Tout IDE Java peut également gérer la compilation et le packaging. Mais pour ce post, nous allons à l'ancienne et le faisons en ligne de commande.

Créez d'abord le fichier Main.javaavec le contenu affiché dans la réponse d'origine.

Pour compiler la partie Java

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

Créez maintenant un fichier avec des méta-informations à ajouter au fichier jar que nous voulons construire. Dans Manifest.txt, ajoutez le texte suivant

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

Maintenant, regroupez tout dans un gros fichier jar, y compris notre programme Clojure et le fichier 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

Pour exécuter le programme:

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

La sortie est essentiellement identique à celle produite par Clojure seul, mais le résultat a été converti en un double Java.

Comme mentionné, un IDE Java s'occupera probablement des arguments de compilation désordonnés et de l'empaquetage.


4
1. Puis- je mettre votre exemple sur clojuredocs.org comme exemple pour la macro "ns"? 2. qu'est-ce que # ^ devant ": methods [# ^ {: static true} [binomial [int int] double]]" (je suis un débutant)?
Belun

5
@Belun, vous pouvez bien sûr l'utiliser comme exemple - je suis flatté. Le "# ^ {: static true}" attache des métadonnées à la fonction indiquant que binomial est une fonction statique. C'est nécessaire dans ce cas car, du côté Java, nous appelons la fonction depuis main - une fonction statique. Si le binôme n'était pas statique, la compilation de la fonction principale du côté Java produirait un message d'erreur indiquant que "la méthode non statique binomial (int, int) ne peut pas être référencée à partir d'un contexte statique". Il existe des exemples supplémentaires sur le site Object Mentor.
clartaq

4
Il y a une chose cruciale qui n'est pas mentionnée ici - pour compiler le fichier Clojure en classe Java, vous devez: (compiler 'com.domain.tiny)
Domchi

Comment compilez-vous AOT la source Clojure? C'est là que je suis coincé.
Matthew Boston

@MatthewBoston Au moment où la réponse a été écrite, j'ai utilisé Enclojure, un plugin pour l'EDI NetBeans. Maintenant, j'utiliserais probablement Leiningen, mais je ne l'ai pas essayé ou testé.
clartaq

119

Depuis Clojure 1.6.0, il existe une nouvelle manière préférée de charger et d'appeler les fonctions Clojure. Cette méthode est maintenant préférée à l'appel direct de RT (et remplace la plupart des autres réponses ici). Le javadoc est ici - le point d'entrée principal est clojure.java.api.Clojure.

Pour rechercher et appeler une fonction Clojure:

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

Les fonctions dans clojure.coresont automatiquement chargées. D'autres espaces de noms peuvent être chargés via require:

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

IFns peut être passé à des fonctions d'ordre supérieur, par exemple, l'exemple ci-dessous passe plusà read:

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

La plupart des IFns dans Clojure font référence à des fonctions. Quelques-uns, cependant, font référence à des valeurs de données non fonctionnelles. Pour y accéder, utilisez à la derefplace de fn:

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

Parfois (si vous utilisez une autre partie du runtime Clojure), vous devrez peut-être vous assurer que le runtime Clojure est correctement initialisé - appeler une méthode sur la classe Clojure est suffisant à cet effet. Si vous n'avez pas besoin d'appeler une méthode sur Clojure, il suffit alors de simplement charger la classe (dans le passé, il y avait une recommandation similaire pour charger la classe RT; c'est maintenant préféré):

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

1
En utilisant cette approche, le script ne peut pas utiliser le guillemet '() et exiger des choses. Y a-t-il une solution à cela?
Renato

2
Je pense qu'il n'y a que quelques formes spéciales qui n'existent pas également en tant que vars. Une solution de contournement consiste à y accéder via Clojure.read("'(1 2 3"). Il serait raisonnable de déposer ceci comme une demande d'amélioration mais pour fournir un Clojure.quote () ou en le faisant fonctionner comme un var.
Alex Miller

1
@Renato Il n'est pas nécessaire de citer quoi que ce soit, car de toute façon rien n'est évalué par les règles d'évaluation de Clojure. Si vous voulez une liste contenant les nombres 1 à 3, alors au lieu d'écrire, '(1 2 3)écrivez quelque chose comme Clojure.var("clojure.core", "list").invoke(1,2,3). Et la réponse a déjà un exemple d'utilisation require: c'est juste une var comme les autres.
amalloy du

34

EDIT Cette réponse a été écrite en 2010, et a fonctionné à cette époque. Voir la réponse d'Alex Miller pour une solution plus moderne.

Quel type de code appelle de Java? Si vous avez une classe générée avec gen-class, appelez-la simplement. Si vous souhaitez appeler une fonction à partir d'un script, regardez l' exemple suivant .

Si vous souhaitez évaluer le code à partir d'une chaîne, à l'intérieur de Java, vous pouvez utiliser le code suivant:

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
Pour développer un peu si vous voulez accéder à def'd var dans l'espace de noms (ie (def my-var 10)) utilisez ceci RT.var("namespace", "my-var").deref().
Ivan Koblik

Cela ne fonctionne pas pour moi sans ajouter RT.load("clojure/core");au début. Quel est le comportement étrange?
hsestupin

Cela fonctionne et est similaire à ce que j'ai utilisé; je ne sais pas si c'est la technique la plus récente. J'utilise peut-être Clojure 1.4 ou 1.6, pas sûr.
Yan King Yin

12

EDIT: J'ai écrit cette réponse il y a presque trois ans. Dans Clojure 1.6, il existe une API appropriée exactement dans le but d'appeler Clojure depuis Java. Veuillez répondre à Alex Miller pour des informations à jour.

Réponse originale de 2011:

Comme je le vois, le moyen le plus simple (si vous ne générez pas de classe avec la compilation AOT) est d'utiliser clojure.lang.RT pour accéder aux fonctions dans clojure. Avec lui, vous pouvez imiter ce que vous auriez fait dans Clojure (pas besoin de compiler les choses de manière spéciale):

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

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

C'est un peu plus verbeux en Java, mais j'espère qu'il est clair que les morceaux de code sont équivalents.

Cela devrait fonctionner tant que Clojure et les fichiers source (ou fichiers compilés) de votre code Clojure sont sur le chemin des classes.


1
Ce conseil est obsolète depuis Clojure 1.6 - veuillez utiliser plutôt clojure.java.api.Clojure.
Alex Miller

Ce n'était pas une mauvaise réponse à l'époque, mais je voulais récompenser stackoverflow.com/a/23555959/1756702 en tant que réponse faisant autorité pour Clojure 1.6. Je vais réessayer demain ...
A. Webb

La réponse d'Alex mérite en effet la prime! S'il vous plaît laissez-moi savoir si je peux vous aider à transférer la prime si nécessaire.
raek

@raek Cela ne me dérange pas que vous ayez un petit bonus de mon doigt de déclenchement sur-caféiné. J'espère vous revoir autour du tag Clojure.
A. Webb

10

Je suis d'accord avec la réponse de clartaq, mais j'ai senti que les débutants pourraient également utiliser:

  • des informations étape par étape sur la façon de faire fonctionner réellement ce
  • informations actuelles pour Clojure 1.3 et les versions récentes de leiningen.
  • un bocal Clojure qui comprend également une fonction principale, de sorte qu'il peut être exécuté de manière autonome ou lié en tant que bibliothèque.

J'ai donc couvert tout cela dans ce billet de blog .

Le code Clojure ressemble à ceci:

(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 configuration du projet leiningen 1.7.1 ressemble à ceci:

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

Le code Java ressemble à ceci:

import ThingOne.*;

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

Ou vous pouvez également obtenir tout le code de ce projet sur github .


Pourquoi avez-vous utilisé l'AOT? Cela ne rend-il plus le programme indépendant de la plate-forme?
Edward

3

Cela fonctionne avec 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 le cas d'utilisation consiste à inclure un JAR construit avec Clojure dans une application Java, j'ai trouvé qu'il était avantageux d'avoir un espace de noms séparé pour l'interface entre les deux mondes:

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

L'espace de noms principal peut utiliser l'instance injectée pour accomplir ses tâches:

(ns example-app.core)

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

À des fins de test, l'interface peut être stubbée:

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

0

Une autre technique qui fonctionne également avec d'autres langages au-dessus de JVM consiste à déclarer une interface pour les fonctions que vous souhaitez appeler, puis à utiliser la fonction «proxy» pour créer une instance qui les implémente.


-1

Vous pouvez également utiliser la compilation AOT pour créer des fichiers de classe représentant votre code de clojure. Lisez la documentation sur la compilation, la classe gen et les amis dans la documentation de l'API Clojure pour plus de détails sur la façon de procéder, mais vous allez essentiellement créer une classe qui appelle des fonctions clojure pour chaque appel de méthode.

Une autre alternative consiste à utiliser la nouvelle fonctionnalité de defprotocol et de deftype, qui nécessitera également la compilation AOT mais offrira de meilleures performances. Je ne connais pas encore les détails sur la façon de procéder, mais une question sur la liste de diffusion ferait probablement l'affaire.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.