Вызов clojure из Java


165

Большинство популярных хитов Google для "вызова clojure из Java" устарели и рекомендуют использовать clojure.lang.RTдля компиляции исходного кода. Не могли бы вы помочь с четким объяснением того, как вызывать Clojure из Java, предполагая, что вы уже создали jar из проекта Clojure и включили его в путь к классам?


8
Я не знаю, что компиляция исходного кода каждый раз «устарела» как таковая. Это дизайнерское решение. Я делаю это сейчас, поскольку это делает интеграцию кода Clojure в унаследованный проект Java Netbeans несложной. Добавьте Clojure в качестве библиотеки, добавьте исходный файл Clojure, настройте вызовы и мгновенную поддержку Clojure, не выполняя несколько этапов компиляции / компоновки! За счет доли секунды задержки при каждом запуске приложения.
Брайан Кноблаух


2
См. Последнюю версию для Clojure 1.8.0 - Clojure теперь имеет прямые ссылки компилятора.
TWR Cole

Ответы:


167

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

Это не так просто, как компилирование в jar и вызов внутренних методов. Хотя, кажется, есть несколько хитростей, чтобы заставить все это работать. Вот пример простого файла Clojure, который можно скомпилировать в 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)))
)

Если вы запустите его, вы должны увидеть что-то вроде:

(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 должна быть на пути класса. В этом примере использовался jar 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, показанную в исходном ответе. (Этот файл был создан, когда Лейнинген создал проект.)

Большая часть волшебства здесь находится в объявлении пространства имен. :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

Лейнинген имеет встроенную задачу, 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 и 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

Чтобы запустить программу:

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

Вывод практически идентичен выводу только Clojure, но результат был преобразован в Java double.

Как уже упоминалось, Java IDE, вероятно, позаботится о беспорядочных аргументах компиляции и упаковке.


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

5
@Belun, конечно, вы можете использовать его в качестве примера - я польщен. «# ^ {: Static true}» присоединяет к метаданным некоторые метаданные, указывающие, что бином является статической функцией. Это необходимо в этом случае, потому что на стороне Java мы вызываем функцию из main - статической функции. Если бы binomial не был статическим, компиляция главной функции на стороне Java выдаст сообщение об ошибке «Нестатический метод binomial (int, int) не может быть вызван из статического контекста». На сайте Object Mentor есть дополнительные примеры.
клартак

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

как у вас дела с компиляцией исходного кода Clojure? Вот где я застрял.
Мэтью Бостон

@MatthewBoston На момент написания ответа я использовал Enclojure, плагин для IDE NetBeans. Теперь я, вероятно, использовал бы Leiningen, но не пробовал и не проверял это.
клартак

119

Начиная с Clojure 1.6.0, появился новый предпочтительный способ загрузки и вызова функций Clojure. Этот метод теперь предпочтительнее прямого вызова RT (и заменяет многие другие ответы здесь). Javadoc здесь - главная точка входа clojure.java.api.Clojure.

Для поиска и вызова функции Clojure:

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

Функции в clojure.coreзагружаются автоматически. Другие пространства имен могут быть загружены с помощью require:

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]"));

Большинство IFnв 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
Используя этот подход, скрипт не может использовать quote '() и требовать что-то. Есть ли решение для этого?
Ренато

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

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

34

РЕДАКТИРОВАТЬ Этот ответ был написан в 2010 году, и работал в то время. Смотрите ответ Алекса Миллера для более современного решения.

Какой код вызывается из Java? Если у вас есть класс, сгенерированный с помощью gen-class, просто вызовите его. Если вы хотите вызвать функцию из скрипта, посмотрите на следующий пример .

Если вы хотите оценить код из строки внутри 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

РЕДАКТИРОВАТЬ: я написал этот ответ почти три года назад. В 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)

И на Яве:

// 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.
Алекс Миллер

Неплохой ответ в то время, но я имел в виду bounty stackoverflow.com/a/23555959/1756702 в качестве авторитетного ответа для Clojure 1.6. Завтра попробую еще раз ...
А. Уэбб

Ответ Алекса действительно заслуживает награды! Пожалуйста, дайте мне знать, если я могу помочь с передачей награды, если это необходимо.
raek

@raek Я не против, что ты получил небольшой бонус от моего спускового пальца с чрезмерным кофеином. Надеюсь увидеть вас снова вокруг тега Clojure.
А. Уэбб

10

Я согласен с ответом клартака, но я чувствовал, что новички также могут использовать:

  • пошаговая информация о том, как на самом деле получить это работает
  • актуальная информация для 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 и друзьях в документации API Clojure для получения подробной информации о том, как это сделать, но по сути вы создадите класс, который вызывает функции clojure для каждого вызова метода.

Другой альтернативой является использование новых функций defprotocol и deftype, которые также потребуют компиляции AOT, но обеспечат более высокую производительность. Я пока не знаю деталей, как это сделать, но вопрос в списке рассылки, вероятно, поможет.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.