Memanggil clojure dari java

165

Sebagian besar hit google teratas untuk "memanggil clojure dari java" sudah usang dan merekomendasikan clojure.lang.RTuntuk menggunakan kompilasi kode sumber. Bisakah Anda membantu dengan penjelasan yang jelas tentang bagaimana memanggil Clojure dari Jawa dengan anggapan Anda telah membangun toples dari proyek Clojure dan memasukkannya ke dalam classpath?

Arthur Ulfeldt
sumber
8
Saya tidak tahu bahwa kompilasi sumber setiap kali "usang" seperti itu. Ini keputusan desain. Saya melakukan itu sekarang karena membuat mengintegrasikan kode Clojure ke proyek Java Netbeans lama. Tambahkan Clojure sebagai perpustakaan, tambahkan file sumber Clojure, atur panggilan, dan dukungan Clojure instan tanpa memiliki beberapa langkah kompilasi / penautan! Dengan biaya sebagian kecil dari penundaan kedua pada setiap awal aplikasi.
Brian Knoblauch
Lihat terbaru untuk Clojure 1.6.0 .
A. Webb
2
Lihat terbaru untuk Clojure 1.8.0 - Clojure sekarang memiliki penautan langsung compiler.
TWR Cole

Jawaban:

167

Pembaruan : Karena jawaban ini diposting, beberapa alat yang tersedia telah berubah. Setelah jawaban asli, ada pembaruan termasuk informasi tentang cara membangun contoh dengan alat saat ini.

Ini tidak sesederhana mengkompilasi ke stoples dan memanggil metode internal. Tampaknya ada beberapa trik untuk membuat semuanya bekerja. Berikut adalah contoh file Clojure sederhana yang dapat dikompilasi ke stoples:

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

Jika Anda menjalankannya, Anda akan melihat sesuatu seperti:

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

Dan inilah program Java yang memanggil -binomialfungsi dalam 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));
    }
}

Outputnya adalah:

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

Bagian pertama sihir menggunakan :methodskata kunci dalam gen-classpernyataan. Tampaknya diperlukan untuk membiarkan Anda mengakses sesuatu fungsi Clojure seperti metode statis di Jawa.

Hal kedua adalah membuat fungsi wrapper yang bisa dipanggil oleh Java. Perhatikan bahwa versi kedua -binomialmemiliki tanda hubung di depannya.

Dan tentu saja toples Clojure sendiri harus berada di jalur kelas. Contoh ini menggunakan stoples Clojure-1.1.0.

Pembaruan : Jawaban ini telah diuji ulang menggunakan alat berikut:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Pembaruan 25

Bagian Clojure

Pertama buat proyek dan struktur direktori terkait menggunakan Leiningen:

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

Sekarang, ubah ke direktori proyek.

C:\projects>cd com.domain.tiny

Dalam direktori proyek, buka project.cljfile dan edit sedemikian rupa sehingga isinya seperti yang ditunjukkan di bawah ini.

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

Sekarang, pastikan semua dependensi (Clojure) tersedia.

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

Anda mungkin melihat pesan tentang mengunduh botol Clojure pada saat ini.

Sekarang edit file Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.cljsedemikian rupa sehingga berisi program Clojure yang ditunjukkan dalam jawaban asli. (File ini dibuat ketika Leiningen menciptakan proyek.)

Banyak keajaiban di sini adalah dalam deklarasi namespace. The :gen-classmemberitahu sistem untuk membuat sebuah kelas bernama com.domain.tinydengan metode statis tunggal yang disebut binomial, fungsi mengambil dua argumen integer dan mengembalikan ganda. Ada dua fungsi yang sama bernama binomial, fungsi Clojure tradisional, dan -binomialdan pembungkus dapat diakses dari Jawa. Perhatikan tanda hubung dalam nama fungsi -binomial. Awalan default adalah tanda hubung, tetapi dapat diubah ke sesuatu yang lain jika diinginkan. The -mainFungsi hanya membuat beberapa panggilan ke fungsi binomial untuk memastikan bahwa kita mendapatkan hasil yang benar. Untuk melakukan itu, kompilasi kelas dan jalankan program.

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

Anda akan melihat output yang ditunjukkan pada jawaban asli.

Sekarang bungkus dalam wadah dan taruh di tempat yang nyaman. Salin toples Clojure di sana juga.

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.

Bagian Jawa

Leiningen memiliki tugas bawaan lein-javac,, yang seharusnya dapat membantu kompilasi Java. Sayangnya, tampaknya rusak di versi 2.1.3. Itu tidak dapat menemukan JDK yang diinstal dan tidak dapat menemukan repositori Maven. Jalur untuk keduanya telah menyematkan ruang pada sistem saya. Saya berasumsi bahwa itulah masalahnya. Setiap IDE Java dapat menangani kompilasi dan pengemasan juga. Tetapi untuk posting ini, kita akan menjadi sekolah tua dan melakukannya di baris perintah.

Pertama buat file Main.javadengan konten yang ditunjukkan dalam jawaban asli.

Untuk mengkompilasi bagian java

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

Sekarang buat file dengan beberapa informasi meta untuk ditambahkan ke toples yang ingin kita buat. Di Manifest.txt, tambahkan teks berikut

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

Sekarang kemas semuanya ke dalam satu file jar besar, termasuk program Clojure kami dan stoples 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

Untuk menjalankan program:

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

Output dasarnya identik dengan yang diproduksi oleh Clojure saja, tetapi hasilnya telah dikonversi menjadi Java double.

Seperti yang disebutkan, Java IDE mungkin akan menangani argumen kompilasi yang berantakan dan pengemasannya.

clartaq
sumber
4
1. Dapatkah saya memberikan contoh Anda di clojuredocs.org sebagai contoh untuk makro "ns"? 2. apa yang # ^ di depan ": methods [# ^ {: static true} [binomial [int int] double]]" (saya seorang pemula)?
Belun
5
@Belun, yakin Anda bisa menggunakannya sebagai contoh - saya tersanjung. "# ^ {: Static true}" menempel beberapa metadata ke fungsi yang menunjukkan bahwa binomial adalah fungsi statis. Ini diperlukan dalam kasus ini karena, di sisi Java, kami memanggil fungsi dari main - fungsi statis. Jika binomial tidak statis, kompilasi fungsi utama di sisi Java akan menghasilkan pesan kesalahan tentang "metode binomial non-statis (int, int) tidak dapat dirujuk dari konteks statis". Ada contoh tambahan di situs Obyek Mentor.
clartaq
4
Ada satu hal penting yang tidak disebutkan di sini - untuk mengkompilasi file Clojure ke kelas Java, Anda perlu: (kompilasi 'com.domain.tiny)
Domchi
bagaimana Anda AOT mengkompilasi sumber Clojure? Di sinilah aku terjebak.
Matthew Boston
@MatthewBoston Pada saat jawabannya ditulis, saya menggunakan Enclojure, sebuah plugin untuk NetBeans IDE. Sekarang, saya mungkin akan menggunakan Leiningen, tetapi belum mencoba atau mengujinya.
clartaq
119

Pada Clojure 1.6.0, ada cara baru yang disukai untuk memuat dan menjalankan fungsi Clojure. Metode ini sekarang lebih disukai daripada memanggil RT secara langsung (dan menggantikan banyak jawaban lain di sini). Javadoc ada di sini - titik masuk utama adalah clojure.java.api.Clojure.

Untuk mencari dan memanggil fungsi Clojure:

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

Fungsi dalam clojure.coredimuat secara otomatis. Ruang nama lain dapat dimuat melalui persyaratan:

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

IFns dapat diteruskan ke fungsi dengan urutan yang lebih tinggi, misalnya contoh di bawah ini diteruskan pluske read:

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

Sebagian besar IFndi Clojure merujuk ke fungsi. Namun, beberapa merujuk pada nilai data yang tidak berfungsi. Untuk mengakses ini, gunakan derefsebagai ganti fn:

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

Kadang-kadang (jika menggunakan beberapa bagian lain dari runtime Clojure), Anda mungkin perlu memastikan bahwa runtime Clojure diinisialisasi dengan benar - memanggil metode pada kelas Clojure sudah cukup untuk tujuan ini. Jika Anda tidak perlu memanggil metode pada Clojure, maka cukup menyebabkan kelas memuat cukup (di masa lalu telah ada rekomendasi serupa untuk memuat kelas RT; ini sekarang lebih disukai):

Class.forName("clojure.java.api.Clojure") 
Alex Miller
sumber
1
Menggunakan pendekatan ini, skrip tidak dapat menggunakan kutipan '() dan memerlukan beberapa hal. Apakah ada solusi untuk itu?
Renato
2
Saya pikir hanya ada beberapa bentuk khusus yang tidak juga ada sebagai vars. Salah satu solusinya adalah mengaksesnya melalui Clojure.read("'(1 2 3"). Akan masuk akal untuk mengajukan ini sebagai permintaan penyempurnaan meskipun untuk memberikan Clojure.quote () atau dengan membuatnya berfungsi sebagai var.
Alex Miller
1
@Renato Tidak perlu mengutip apa pun, karena tidak ada yang dievaluasi oleh aturan evaluasi Clojure. Jika Anda ingin daftar yang berisi angka 1-3, daripada menulis '(1 2 3)Anda menulis sesuatu seperti Clojure.var("clojure.core", "list").invoke(1,2,3). Dan jawabannya sudah memiliki contoh cara menggunakan require: itu hanya var seperti yang lain.
amalloy
34

EDIT Jawaban ini ditulis pada tahun 2010, dan berfungsi pada saat itu. Lihat jawaban Alex Miller untuk solusi yang lebih modern.

Kode apa yang ditelepon dari Jawa? Jika Anda memiliki kelas yang dihasilkan dengan gen-class, cukup panggil saja. Jika Anda ingin memanggil fungsi dari skrip, lihat contoh berikut .

Jika Anda ingin mengevaluasi kode dari string, di dalam Java, maka Anda dapat menggunakan kode berikut:

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
sumber
1
Untuk memperluas sedikit jika Anda ingin mengakses def'd var di namespace (yaitu (def my-var 10)) gunakan ini RT.var("namespace", "my-var").deref().
Ivan Koblik
Itu tidak berfungsi untuk saya tanpa menambahkan RT.load("clojure/core");di awal. Apa kelakuannya yang aneh?
hsestupin
Ini berfungsi dan mirip dengan apa yang saya gunakan; tidak yakin apakah itu teknik terbaru. Saya mungkin menggunakan Clojure 1.4 atau 1.6, tidak yakin.
Yan King Yin
12

EDIT: Saya menulis jawaban ini hampir tiga tahun yang lalu. Di Clojure 1.6 ada API yang tepat persis untuk tujuan memanggil Clojure dari Jawa. Harap jawaban Alex Miller untuk informasi terbaru.

Jawaban asli dari 2011:

Seperti yang saya lihat, cara paling sederhana (jika Anda tidak menghasilkan kelas dengan kompilasi AOT) adalah dengan menggunakan clojure.lang.RT untuk mengakses fungsi-fungsi di clojure. Dengan itu Anda dapat meniru apa yang akan Anda lakukan di Clojure (tidak perlu mengkompilasi dengan cara khusus):

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

Dan di Jawa:

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

Ini sedikit lebih verbose di Jawa, tapi saya harap jelas bahwa potongan-potongan kode setara.

Ini harus berfungsi selama Clojure dan file sumber (atau file yang dikompilasi) dari kode Clojure Anda ada di classpath.

raek
sumber
1
Nasihat ini sudah ketinggalan zaman pada Clojure 1.6 - silakan gunakan clojure.java.api.Clojure sebagai gantinya.
Alex Miller
Bukan jawaban yang buruk pada saat itu, tetapi saya bermaksud memberi karunia stackoverflow.com/a/23555959/1756702 sebagai jawaban resmi untuk Clojure 1.6. Akan mencoba lagi besok ...
A. Webb
Jawaban Alex memang pantas mendapatkan hadiah! Harap beri tahu saya jika saya dapat membantu mentransfer hadiah jika diperlukan.
raek
@raek, saya tidak keberatan Anda mendapat sedikit bonus dari jari pemicu saya yang terlalu banyak berkafein. Berharap dapat melihat Anda di sekitar tag Clojure lagi.
A. Webb
10

Saya setuju dengan jawaban clartaq, tetapi saya merasa bahwa pemula juga dapat menggunakan:

  • informasi langkah-demi-langkah tentang cara menjalankan ini
  • informasi yang terkini untuk Clojure 1.3 dan leiningen versi terbaru.
  • stoples Clojure yang juga mencakup fungsi utama, sehingga dapat dijalankan sendiri atau dihubungkan sebagai perpustakaan.

Jadi saya membahas semua itu di posting blog ini .

Kode Clojure terlihat seperti ini:

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

Setup proyek leiningen 1.7.1 terlihat seperti ini:

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

Kode Java terlihat seperti ini:

import ThingOne.*;

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

Atau Anda juga bisa mendapatkan semua kode dari proyek ini di github .

sandover
sumber
Mengapa Anda menggunakan AOT? Bukankah itu menyebabkan program tidak lagi platform-independet?
Edward
3

Ini bekerja dengan 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
sumber
2

Jika use case adalah untuk memasukkan JAR yang dibangun dengan Clojure dalam aplikasi Java, saya menemukan memiliki namespace terpisah untuk antarmuka antara dua dunia yang bermanfaat:

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

Namespace inti dapat menggunakan contoh yang disuntikkan untuk menyelesaikan tugasnya:

(ns example-app.core)

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

Untuk tujuan pengujian, antarmuka dapat dihapus:

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

Teknik lain yang berfungsi juga dengan bahasa lain di atas JVM adalah mendeklarasikan antarmuka untuk fungsi yang ingin Anda panggil dan kemudian menggunakan fungsi 'proxy' untuk membuat instance yang mengimplementasikannya.

Petr Gladkikh
sumber
-1

Anda juga dapat menggunakan kompilasi AOT untuk membuat file kelas yang mewakili kode clojure Anda. Baca dokumentasi tentang kompilasi, kelas-gen, dan teman-teman di dokumen Clojure API untuk detail tentang cara melakukan ini, tetapi pada dasarnya Anda akan membuat kelas yang memanggil fungsi clojure untuk setiap pemanggilan metode.

Alternatif lain adalah dengan menggunakan defprotocol baru dan fungsionalitas deftype, yang juga akan memerlukan kompilasi AOT tetapi memberikan kinerja yang lebih baik. Saya belum tahu detail bagaimana melakukan ini, tetapi sebuah pertanyaan di milis mungkin akan berhasil.

rosejn
sumber