Wywołanie clojure z java

165

Większość najpopularniejszych haseł Google dotyczących „wywoływania clojure z javy” jest nieaktualna i zaleca się użycie ich clojure.lang.RTdo kompilacji kodu źródłowego. Czy mógłbyś pomóc w jasnym wyjaśnieniu, jak wywołać Clojure z Java, zakładając, że masz już słoik z projektu Clojure i umieściłeś go w ścieżce klas?

Arthur Ulfeldt
źródło
8
Nie wiem, czy kompilowanie źródeł za każdym razem jest „nieaktualne” jako takie. To decyzja projektowa. Robię to teraz, ponieważ umożliwia to integrację kodu Clojure ze starszym projektem Java Netbeans w mgnieniu oka. Dodaj Clojure jako bibliotekę, dodaj plik źródłowy Clojure, skonfiguruj połączenia i natychmiastową obsługę Clojure bez konieczności wykonywania wielu kroków kompilacji / łączenia! Kosztem ułamka sekundy opóźnienia przy każdym uruchomieniu aplikacji.
Brian Knoblauch
2
Zobacz najnowsze informacje o Clojure 1.8.0 - Clojure ma teraz bezpośrednie połączenie z kompilatorem.
TWR Cole

Odpowiedzi:

167

Aktualizacja : od czasu opublikowania tej odpowiedzi niektóre z dostępnych narzędzi uległy zmianie. Po oryginalnej odpowiedzi następuje aktualizacja zawierająca informacje o tym, jak zbudować przykład przy użyciu aktualnych narzędzi.

Nie jest to tak proste, jak kompilacja do pliku jar i wywołanie metod wewnętrznych. Wydaje się jednak, że jest kilka sztuczek, które sprawiają, że wszystko działa. Oto przykład prostego pliku Clojure, który można skompilować do pliku 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)))
)

Jeśli go uruchomisz, powinieneś zobaczyć coś takiego:

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

A oto program Java, który wywołuje -binomialfunkcję w 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));
    }
}

Jego wynik to:

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

Pierwsza magia polega na użyciu :methodssłowa kluczowego w gen-classinstrukcji. Wydaje się, że jest to wymagane, aby umożliwić dostęp do funkcji Clojure, podobnej do metod statycznych w Javie.

Drugą rzeczą jest utworzenie funkcji opakowującej, którą może wywołać Java. Zauważ, że druga wersja -binomialma przed sobą myślnik.

I oczywiście sam jar Clojure musi znajdować się na ścieżce klas. W tym przykładzie użyto jar Clojure-1.1.0.

Aktualizacja : ta odpowiedź została ponownie przetestowana przy użyciu następujących narzędzi:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 aktualizacja 25

Część Clojure

Najpierw utwórz projekt i powiązaną strukturę katalogów za pomocą Leiningen:

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

Teraz przejdź do katalogu projektu.

C:\projects>cd com.domain.tiny

W katalogu projektu otwórz project.cljplik i edytuj go tak, aby zawartość była taka, jak pokazano poniżej.

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

Teraz upewnij się, że wszystkie zależności (Clojure) są dostępne.

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

W tym momencie może pojawić się komunikat o pobieraniu słoika Clojure.

Teraz edytuj plik Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.cljtak, aby zawierał program Clojure pokazany w oryginalnej odpowiedzi. (Ten plik został utworzony, gdy Leiningen utworzył projekt).

Duża część magii tkwi w deklaracji przestrzeni nazw. :gen-classInformuje system, aby utworzyć klasę o nazwie com.domain.tinyz jednej metody statycznej nazwie binomial, funkcja biorąc dwa argumenty całkowite i powracający podwójne. Istnieją dwie podobnie nazwane funkcje binomial, tradycyjna funkcja Clojure -binomiali opakowanie dostępne z poziomu języka Java. Zwróć uwagę na myślnik w nazwie funkcji -binomial. Domyślnym przedrostkiem jest myślnik, ale w razie potrzeby można go zmienić na inny. -mainFunkcja po prostu sprawia, że kilka wywołań funkcji dwumianowego, aby zapewnić, że jesteśmy coraz poprawnych wyników. Aby to zrobić, skompiluj klasę i uruchom program.

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

Powinieneś zobaczyć wynik pokazany w oryginalnej odpowiedzi.

Teraz zapakuj go do słoika i umieść w dogodnym miejscu. Skopiuj tam również słoik 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.

Część Java

Leiningen ma wbudowane zadanie, lein-javacktóre powinno pomóc przy kompilacji Javy. Niestety wydaje się, że w wersji 2.1.3 jest uszkodzony. Nie może znaleźć zainstalowanego JDK i nie może znaleźć repozytorium Maven. Ścieżki do obu mają osadzone spacje w moim systemie. Zakładam, że w tym tkwi problem. Każde środowisko Java IDE również poradzi sobie z kompilacją i pakowaniem. Ale w tym poście idziemy do starej szkoły i robimy to w wierszu poleceń.

Najpierw utwórz plik Main.javaz zawartością przedstawioną w oryginalnej odpowiedzi.

Aby skompilować część java

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

Teraz utwórz plik z kilkoma metainformacjami, aby dodać je do jar, który chcemy zbudować. W Manifest.txt, dodaj następujący tekst

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

Teraz zapakuj to wszystko w jeden duży plik jar, w tym nasz program Clojure i 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

Aby uruchomić program:

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

Dane wyjściowe są zasadniczo identyczne z danymi wygenerowanymi przez sam Clojure, ale wynik został przekonwertowany na podwójną Java.

Jak wspomniano, środowisko Java IDE prawdopodobnie zajmie się niechlujnymi argumentami kompilacji i opakowaniem.

clartaq
źródło
4
1. Czy mogę umieścić Twój przykład w witrynie clojuredocs.org jako przykład makra „ns”? 2. co znajduje się # ^ przed ": methods [# ^ {: static true} [dwumian [int] double]]" (jestem nowicjuszem)?
Belun
5
@Belun, na pewno możesz użyć tego jako przykładu - pochlebia mi to. „# ^ {: Static true}” dołącza pewne metadane do funkcji, wskazując, że dwumian jest funkcją statyczną. Jest to wymagane w tym przypadku, ponieważ po stronie Javy wywołujemy funkcję z main - funkcję statyczną. Gdyby dwumian nie był statyczny, kompilacja funkcji głównej po stronie języka Java spowodowałaby komunikat o błędzie „Nie można odwoływać się do metody dwumianowej niestatycznej (int, int) z kontekstu statycznego”. Dodatkowe przykłady można znaleźć w witrynie Object Mentor.
clartaq
4
Jest jedna ważna rzecz, o której tutaj nie wspomniano - aby skompilować plik Clojure do klasy Java, musisz: (skompilować 'com.domain.tiny)
Domchi
jak kompilujesz źródło Clojure w AOT? Tu utknąłem.
Matthew Boston
@MatthewBoston W czasie pisania odpowiedzi korzystałem z Enclojure, wtyczki do NetBeans IDE. Teraz prawdopodobnie użyłbym Leiningen, ale nie wypróbowałem go ani nie przetestowałem.
clartaq
119

Od Clojure 1.6.0 istnieje nowy preferowany sposób ładowania i wywoływania funkcji Clojure. Ta metoda jest teraz preferowana do bezpośredniego wywoływania RT (i zastępuje wiele innych odpowiedzi tutaj). Javadoc jest tutaj - głównym punktem wejścia jest clojure.java.api.Clojure.

Aby wyszukać i wywołać funkcję Clojure:

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

Funkcje w programie clojure.coresą ładowane automatycznie. Inne przestrzenie nazw mogą być ładowane przez require:

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

IFns można przekazać do funkcji wyższego rzędu, np. poniższy przykład przechodzi plusdo read:

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

Większość IFnznaków w Clojure odnosi się do funkcji. Jednak kilka z nich odnosi się do wartości danych niezwiązanych z funkcją. Aby uzyskać do nich dostęp, użyj derefzamiast fn:

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

Czasami (jeśli używasz innej części środowiska uruchomieniowego Clojure), może być konieczne upewnienie się, że środowisko uruchomieniowe Clojure zostało poprawnie zainicjowane - wystarczy do tego celu wywołanie metody z klasy Clojure. Jeśli nie musisz wywoływać metody w Clojure, wystarczy po prostu spowodować załadowanie klasy (w przeszłości było podobne zalecenie, aby załadować klasę RT; teraz jest to preferowane):

Class.forName("clojure.java.api.Clojure") 
Alex Miller
źródło
1
Używając tego podejścia, skrypt nie może używać quote '() i wymagać pewnych rzeczy. Czy jest na to rozwiązanie?
Renato
2
Myślę, że jest tylko kilka specjalnych form, które nie istnieją również jako zmienne. Jednym obejściem jest uzyskanie do niego dostępu za pośrednictwem Clojure.read("'(1 2 3"). Rozsądne byłoby zgłoszenie tego jako prośby o ulepszenie, aby dostarczyć Clojure.quote () lub sprawić, by działał jako var.
Alex Miller,
1
@Renato Nie ma potrzeby niczego cytować, ponieważ i tak nic nie jest oceniane przez reguły oceny Clojure. Jeśli chcesz listę zawierającą cyfry 1-3, zamiast pisać '(1 2 3), napisz coś w stylu Clojure.var("clojure.core", "list").invoke(1,2,3). A odpowiedź zawiera już przykład użycia require: to po prostu var, jak każdy inny.
amalloy
34

EDYTUJ Ta odpowiedź została napisana w 2010 roku i działała w tamtym czasie. Zobacz odpowiedź Alexa Millera na bardziej nowoczesne rozwiązanie.

Jaki kod wywołuje z Java? Jeśli masz klasę wygenerowaną za pomocą klasy gen, po prostu ją wywołaj. Jeśli chcesz wywołać funkcję ze skryptu, spójrz na następujący przykład .

Jeśli chcesz ocenić kod ze stringów w Javie, możesz użyć następującego kodu:

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
źródło
1
Aby nieco rozwinąć, jeśli chcesz uzyskać dostęp do zmiennej def'd w przestrzeni nazw (tj. (Def my-var 10)) użyj tego RT.var("namespace", "my-var").deref().
Ivan Koblik
U mnie nie działa bez dodania RT.load("clojure/core");na początku. Co za dziwne zachowanie?
hsestupin
To działa i jest podobne do tego, czego używałem; nie jestem pewien, czy to najnowsza technika. Być może używam Clojure 1.4 lub 1.6, nie jestem pewien.
Yan King Yin
12

EDYCJA: Napisałem tę odpowiedź prawie trzy lata temu. W Clojure 1.6 znajduje się odpowiednie API, które służy do wywoływania Clojure z Javy. Proszę o odpowiedź Alexa Millera, aby uzyskać aktualne informacje.

Oryginalna odpowiedź z 2011 r .:

Jak widzę, najprostszym sposobem (jeśli nie generujesz klasy z kompilacją AOT) jest użycie clojure.lang.RT w celu uzyskania dostępu do funkcji w clojure. Dzięki niemu możesz naśladować to, co zrobiłbyś w Clojure (nie ma potrzeby kompilowania rzeczy w specjalny sposób):

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

A w Javie:

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

W Javie jest trochę bardziej rozwlekły, ale mam nadzieję, że jest jasne, że fragmenty kodu są równoważne.

Powinno to działać, o ile Clojure i pliki źródłowe (lub pliki skompilowane) twojego kodu Clojure znajdują się w ścieżce klas.

raek
źródło
1
Ta rada jest nieaktualna od Clojure 1.6 - zamiast tego użyj clojure.java.api.Clojure.
Alex Miller
W tamtym czasie niezła odpowiedź, ale chciałem nagrodzić stackoverflow.com/a/23555959/1756702 jako autorytatywną odpowiedź dla Clojure 1.6. Spróbuję ponownie jutro ...
A. Webb
Odpowiedź Alexa rzeczywiście zasługuje na nagrodę! Daj mi znać, jeśli w razie potrzeby mogę pomóc w przeniesieniu nagrody.
raek
@raek Nie przeszkadza mi to, że dostałeś małą premię z mojego nadmiernie kofeinowego palca na spust. Mam nadzieję, że zobaczymy się ponownie przy tagu Clojure.
A. Webb
10

Zgadzam się z odpowiedzią clartaq, ale czułem, że początkujący mogą również skorzystać z:

  • informacje krok po kroku, jak właściwie to uruchomić
  • informacje, które są aktualne dla Clojure 1.3 i najnowszych wersji leiningen.
  • słoik Clojure, który zawiera również główną funkcję, dzięki czemu można go uruchomić samodzielnie lub połączyć jako bibliotekę.

Więc opisałem to wszystko w tym wpisie na blogu .

Kod Clojure wygląda następująco:

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

Konfiguracja projektu w Leiningen 1.7.1 wygląda następująco:

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

Kod Java wygląda następująco:

import ThingOne.*;

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

Możesz też pobrać cały kod z tego projektu na github .

sandover
źródło
Dlaczego użyłeś AOT? Czy to nie powoduje, że program nie jest już niezależny od platformy?
Edward
3

Działa to z 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
źródło
2

Jeśli przypadkiem użycia jest włączenie JARa zbudowanego za pomocą Clojure w aplikacji Java, zauważyłem, że korzystna jest oddzielna przestrzeń nazw dla interfejsu między dwoma światami:

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

Podstawowa przestrzeń nazw może używać wstrzykniętej instancji do wykonywania swoich zadań:

(ns example-app.core)

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

Do celów testowych interfejs można zablokować:

(example-app.core/send-mails 
  (reify com.example.WeatherForecast (getTemperature [this] ...)))
jstaffans
źródło
0

Inną techniką, która działa również z innymi językami poza JVM, jest zadeklarowanie interfejsu dla funkcji, które chcesz wywołać, a następnie użycie funkcji „proxy” w celu utworzenia instancji, która je implementuje.

Petr Gladkikh
źródło
-1

Możesz również użyć kompilacji AOT do tworzenia plików klas reprezentujących kod clojure. Przeczytaj dokumentację dotyczącą kompilacji, klasy gen i przyjaciół w dokumentacji Clojure API, aby uzyskać szczegółowe informacje o tym, jak to zrobić, ale w istocie utworzysz klasę, która wywoła funkcje clojure dla każdego wywołania metody.

Inną alternatywą jest użycie nowych funkcji defprotocol i deftype, które również będą wymagały kompilacji AOT, ale zapewni lepszą wydajność. Nie znam jeszcze szczegółów, jak to zrobić, ale pytanie na liście mailingowej prawdopodobnie by załatwiło sprawę.

rosejn
źródło