Виклик клоджура з Java


165

Більшість найпопулярніших звернень google для "виклику клоджура з Java" застаріли, і рекомендується використовувати clojure.lang.RTдля компіляції вихідного коду. Чи можете ви допомогти з чітким поясненням того, як викликати Clojure з Java, якщо ви вже створили банку з проекту Clojure і включили його в класний шлях?


8
Я не знаю, що компіляція джерела щоразу "застаріла" як така. Це дизайнерське рішення. Я це роблю зараз, оскільки це робить інтеграцію коду Clojure у застарілий проект Java Netbeans. Додайте Clojure як бібліотеку, додайте вихідний файл Clojure, налаштуйте дзвінки та миттєву підтримку Clojure, не маючи декількох етапів компіляції / зв’язку! Ціною частки секунди затримки кожного запуску програми.
Брайан Ноблеуч


2
Дивіться останню інформацію про Clojure 1.8.0 - Clojure тепер має пряме посилання компілятора.
TWR Коул

Відповіді:


167

Оновлення : з моменту опублікування цієї відповіді деякі доступні інструменти змінилися. Після оригінальної відповіді з'являється оновлення, що включає інформацію про те, як побудувати приклад за допомогою поточних інструментів.

Це не так просто, як компілювати в банку і викликати внутрішні методи. Здається, є кілька хитрощів, щоб все це працювало. Ось приклад простого файлу Clojure, який можна зібрати в банку:

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

Якщо ви запустите його, ви повинні побачити щось на кшталт:

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

А ось програма Java, яка викликає -binomialфункцію в 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));
    }
}

Його вихід:

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

Перший фрагмент магії - це використання :methodsключового слова у gen-classвиписці. Це, здається, потрібно для отримання доступу до функції Clojure на зразок статичних методів на Java.

Друге - створити функцію обгортки, яку може викликати Java. Зауважте, що друга версія -binomialмає тире перед собою.

І звичайно, сама банка Clojure повинна бути на уроці. У цьому прикладі використано банку Clojure-1.1.0.

Оновлення : Ця відповідь була повторно перевірена за допомогою наступних інструментів:

  • Clojure 1.5.1
  • Лейнінген 2.1.3
  • Оновлення JDK 1.7.0 25

Частина Clojure

Спочатку створіть проект та пов'язану з ним структуру каталогів за допомогою Leiningen:

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

Тепер перейдіть до каталогу проектів.

C:\projects>cd com.domain.tiny

У каталозі проекту відкрийте project.cljфайл і відредагуйте його таким чином, щоб вміст був таким, як показано нижче.

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

Тепер переконайтесь, що всі залежності (Clojure) доступні.

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

Ви можете побачити повідомлення про завантаження банку Clojure в цей момент.

Тепер відредагуйте файл Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.cljтаким чином, щоб він містив програму Clojure, показану в оригінальній відповіді. (Цей файл був створений, коли Leiningen створив проект.)

Велика частина магії тут полягає в оголошенні простору імен. :gen-classКаже системі , щоб створити клас з ім'ям com.domain.tinyза допомогою одного методу статичної називається binomial, функція приймає два цілочисельних аргументу і повертає в два рази. Є дві аналогічно названі функції binomial, традиційна функція Clojure, -binomialі обгортка, доступна від Java. Зверніть увагу на дефіс у назві функції -binomial. Префікс за замовчуванням - дефіс, але за бажанням його можна змінити на щось інше. Ця -mainфункція робить кілька викликів до біноміальної функції, щоб переконатися, що ми отримуємо правильні результати. Для цього складіть клас та запустіть програму.

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

Ви повинні побачити вихід, показаний у вихідній відповіді.

Тепер упакуйте його в банку і покладіть кудись зручно. Скопіюйте туди також банку 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.

Частина Java

Leiningen має вбудоване завдання, lein-javacяке має бути в змозі допомогти з компіляцією Java. На жаль, він, здається, порушений у версії 2.1.3. Він не може знайти встановлений JDK і не може знайти сховище Maven. Шляхи до обох мають вбудовані пробіли в моїй системі. Я припускаю, що це проблема. Будь-який Java IDE також може працювати з компіляцією та упаковкою. Але на цій посаді ми переходимо до старої школи і робимо це в командному рядку.

Спочатку створіть файл Main.javaіз вмістом, показаним у вихідній відповіді.

Складати частину Java

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

Тепер створимо файл з деякою метаінформацією, яку слід додати до баночки, яку ми хочемо створити. В Manifest.txt, додайте наступний текст

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

Тепер упакуйте все це в один великий файл jar, включаючи нашу програму Clojure та банку 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

Щоб запустити програму:

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

Вихід по суті ідентичний тому, який виробляється лише Clojure, але результат був перетворений в подвійний Java.

Як уже згадувалося, Java IDE, ймовірно, подбає про безладні аргументи компіляції та упаковку.


4
1. Чи можу я поставити ваш приклад на clojuredocs.org як приклад для макросу "ns"? 2. що є # ^ перед ": method [# ^ {: static true} [binomial [int int] double]]" (я новачок)?
Белун

5
@Belun, впевнений, що ти можеш використовувати це як приклад - я лестощів. "# ^ {: Статична правда}" приєднує деякі метадані до функції, що вказує на те, що двочлен є статичною функцією. У цьому випадку це потрібно, тому що, з боку Java, ми викликаємо функцію з main - статичної функції. Якщо двочлен не був статичним, компіляція основної функції на стороні Java створює повідомлення про помилку про те, що "нестатичний метод біноміалу (int, int) не може бути посилається на статичний контекст". На веб-сайті Obent Mentor є додаткові приклади.
clartaq

4
Тут не згадується одна важлива річ - щоб скласти файл Clojure до класу Java, потрібно: (компілювати 'com.domain.tiny)
Domchi

як ти AOT збираєш джерело Clojure? Тут я застряг.
Меттью Бостон

@MatthewBoston Під час написання відповіді я використав Enclojure, плагін для IDE NetBeans. Зараз я б, мабуть, користувався Лейнінгеном, але не пробував і не перевіряв.
clartaq

119

Станом на Clojure 1.6.0, є новий бажаний спосіб завантаження та виклику функцій Clojure. Цей метод тепер вважає за краще прямий виклик RT (і замінює багато інших відповідей тут). Явадок тут - головна точка входу clojure.java.api.Clojure.

Для пошуку та виклику функції Clojure:

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

Функції в clojure.coreавтоматично завантажуються. Інші простори імен можна завантажувати за допомогою вимагає:

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

IFns може бути передано до функцій вищого порядку, наприклад, приклад нижче переходить plusдо read:

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

Більшість IFns у Clojure відносяться до функцій. Однак деякі з них відносяться до нефункціональних значень даних. Для доступу до них використовуйте derefзамість fn:

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

Іноді (якщо використовується якась інша частина часу Clojure), можливо, вам доведеться переконатися, що час виконання Clojure належним чином ініціалізується - для цього достатньо викликати метод класу Clojure. Якщо вам не потрібно викликати метод на Clojure, достатньо просто змусити клас завантажуватись (раніше минула аналогічна рекомендація щодо завантаження класу RT; зараз це бажано):

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

1
Використовуючи такий підхід, сценарій не може використовувати quo '() і вимагати речей. Чи є для цього рішення?
Ренато

2
Я думаю, що існує лише пара спеціальних форм, які також не існують як вар. Одне вирішення - це отримати доступ до нього через Clojure.read("'(1 2 3"). Було б розумним подати це як запит на вдосконалення, хоча надати Clojure.quote () або змусивши його працювати як вар.
Алекс Міллер

1
@Renato Не потрібно нічого цитувати, тому що нічого не оцінюють правила оцінки Clojure. Якщо ви хочете список, що містить цифри 1-3, то замість того, щоб писати, '(1 2 3)ви пишете щось на кшталт Clojure.var("clojure.core", "list").invoke(1,2,3). І у відповіді вже є приклад, як користуватися require: це просто вар, як і будь-який інший.
amalloy

34

EDIT Ця відповідь була написана в 2010 році і працювала на той час. Дивіться відповідь Алекса Міллера для більш сучасного рішення.

Який код дзвонить з Java? Якщо у вас є клас, створений за допомогою ген-класу, просто зателефонуйте йому. Якщо ви хочете викликати функцію зі скрипту, перегляньте наступний приклад .

Якщо ви хочете оцінити код з рядка, всередині Java, ви можете використовувати наступний код:

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
Щоб трохи розширити, якщо ви хочете отримати доступ до def'd var у просторі імен (тобто (def my-var 10)), використовуйте це RT.var("namespace", "my-var").deref().
Іван Коблик

Це не працює для мене без додавання RT.load("clojure/core");на початку. Яка дивна поведінка?
hsestupin

Це працює і схоже на те, що я використовував; не впевнений, чи це новітня техніка. Можливо, я використовую Clojure 1.4 або 1.6, не впевнений.
Ян Кінг Інь

12

EDIT: Цю відповідь я написав майже три роки тому. У Clojure 1.6 є належний API саме для виклику Clojure з Java. Будь ласка, отримайте відповідь Алекса Міллера про актуальну інформацію.

Оригінальна відповідь від 2011 року:

Як я бачу, найпростіший спосіб (якщо ви не генеруєте клас з компіляцією AOT) - використовувати clojure.lang.RT для доступу до функцій clojure. З його допомогою ви можете імітувати те, що ви зробили б у Clojure (не потрібно складати речі спеціальними способами):

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

І на 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);

Це трохи докладніше в Java, але я сподіваюся, що зрозуміло, що фрагменти коду рівноцінні.

Це повинно працювати до тих пір, поки Clojure і вихідні файли (або компільовані файли) вашого коду Clojure перебувають на класі.


1
Ця порада застаріла з Clojure 1.6 - використовуйте замість неї Clojure.java.api.Clojure.
Алекс Міллер

На той час не погана відповідь, але я мав намір виграти stackoverflow.com/a/23555959/1756702 як авторитетну відповідь для Clojure 1.6. Спробую знову завтра ...
А. Вебб

Відповідь Алекса справді заслуговує на винагороду! Будь ласка, дайте мені знати, чи можу я допомогти з перерахуванням коштів, якщо потрібно.
raek

@raek Я не заперечую, що ти отримав невеликий бонус від мого пальця, що переоцінив кофеїн Сподіваюсь знову побачити вас навколо тегу Clojure.
А. Вебб

10

Я погоджуюся з відповіддю clartaq, але відчував, що початківці також можуть використовувати:

  • покрокова інформація про те, як реально запустити це
  • інформація, актуальна для Clojure 1.3 та останніх версій leiningen.
  • банку Clojure, яка також включає в себе основну функцію, тому її можна запускати окремо або зв’язувати як бібліотеку.

Тож я висвітлював усе це у цій публікації в блозі .

Код Clojure виглядає приблизно так:

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

Налаштування проекту leiningen 1.7.1 виглядає приблизно так:

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

Код Java виглядає приблизно так:

import ThingOne.*;

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

Або ви також можете отримати весь код з цього проекту на github .


Чому ви використовували AOT? Хіба це не призводить до того, що програма вже не є платформою незалежною?
Едвард

3

Це працює з 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

Якщо випадок використання включає JAR, побудований разом з Clojure, у додатку Java, я виявив, що окремий простір імен для інтерфейсу між двома світами є корисним:

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

Простір імен ядра може використовувати введений екземпляр для виконання своїх завдань:

(ns example-app.core)

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

Для тестових цілей інтерфейс можна заглушити:

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

0

Інша методика, яка працює також з іншими мовами поверх JVM - це оголосити інтерфейс для функцій, які ви хочете викликати, а потім використовувати функцію 'proxy' для створення примірника, який їх реалізує.


-1

Ви також можете використовувати компіляцію AOT для створення файлів класів, що представляють ваш код clojure. Прочитайте документацію про компіляцію, gen-class та друзів у документах API Clojure, щоб отримати детальну інформацію про те, як це зробити, але по суті ви створите клас, який викликає функції clojure для кожного виклику методу.

Іншою альтернативою є використання нового функціоналу defprotocol і deftype, що також вимагатиме компіляції AOT, але забезпечує кращу ефективність. Я ще не знаю деталей, як це зробити, але питання в списку розсилки, ймовірно, зробить свою справу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.