Интегрируем clojure-библиотеку в java-приложение

JavaLispClojure
Язык Clojure отличается очень тесной интеграцией с Java. Прямое использование Java-библиотеки в приложении на Clojure — дело совершенно простое и обыденное. Обратная интеграция несколько сложнее. В этой статье указаны некоторые варианты интеграции кода на Clojure в Java-приложение.

В качестве примера возьмем следующий код:

(ns clj-lib.core
  (:use clj-lib.util))

(defn prod
  ([x] (reduce * x))
  ([s x] (reduce * s x)))

(defprotocol IAppendable
  (append [this value]))

(extend-protocol IAppendable
  Integer (append [this value] (+ this value))
  String (append [this value] (str this "," value)))

(defmulti pretty type)
(defmethod pretty Integer [x] (str "int " x))
(defmethod pretty String [x] (str "str " x))

Тут нету глобальных переменных, в случае необходимости для них можно создать отдельные get-функции. Объявлен мультиметод и протокол — их также можно использовать из Java.

Стандартные интерфейсы Java


Clojure использует свою реализацию стандартных структур, со своими интерфейсами. Для пущего удобства все стандартные коллекции реализуют интерфейсы из java.util.*. Например, все последовательности (списки, вектора, даже ленивые последовательности) реализуют интерфейс java.util.List. Разумеется, все «мутирующие» методы (add, clear и т.п.) просто выбрасывают исключение UnsupportedOperationException. Аналогично с множествами и словарями — они реализуют Set и Map соответственно.

Все функции реализуют 2 стандартных интерфейса java.lang.Runnable и java.util.concurrent.Callable. Это может быть удобно при прямой работе с java.lang.concurent (хотя, скорее всего, лучше с executor'ами работать прямо из Clojure).

При работе с длинной арифметикой важно помнить, что в Clojure свой тип для длинных целых clojure.lang.BigInt. При этом Clojure умеет работать и со стандартным java.util.math.BigInteger. С длинными вещественными такого «сюрприза» нету — используется стандартный java.util.math.BigDecimal. Также есть специальный тип clojure.lang.Ratio для рациональных дробей.

Компиляция и gen-class


Наверное, самый очевидный вариант — скомпилировать clojure-код и получить набор class-файлов. Добавляем команду gen-class в объявление нашего неймспейса, указываем сигнатуры для функций:

(ns clj-lib.core
  (:use clj-lib.util)
  (:gen-class
    :main false
    :name "cljlib.CljLibCore"
    :prefix ""
    :methods 
    [^:static [prod [Iterable] Number]
     ^:static [prod [Number Iterable] Number]
     ^:static [append [Object Object] Object]
     ^:static [pretty [Object] Object]]))
...


Тут мы указали Iterable как тип аргумента для функции prod. На самом деле туда можно передать и ISeq, и массив, и даже String. Но, скорее всего, в Java удобнее будет работать именно с этим интерфейсом.
Имя класса можно выбрать любое.
Если параметр не указать, то будет использовано clj_lib.core. Для протокола будет сгенерирован класс clj_lib.core.IAppendable в пакете clj_lib.core. Т.е у нас будет класс и пакет с одинаковым именем. Поэтому лучше указать имя класса явно.

После этого нужно скомпилировать неймспейс. Выполняем в REPL'е:
(compile 'clj-lib.core)

Получаем файл classes/cljlib/CljLibCore.class, который можно напрямую использовать в нашем приложении. Но компилировать из REPL-а откровенно неудобно, поэтому лучше настроить leiningen проект:
(defproject clj-lib
  ...
  :aot [my-app.core],
  ...
  )

Теперь при создании jar-ок leiningen будет автоматически компилировать указанный неймспейс. Выполняем команду:
lein jar

Подключаем my-lib.jar и clojure.jar к нашему Java-проекту и используем:
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;

import clj_lib.core.IAppendable;
import cljlib.CljLibCore;

public class Program {

	static void pr(Object v) {
		System.out.println(v);
	}
	
	static class SomeClass implements IAppendable {
		public Object append(Object value) {
			// some code
			return null;  
		}
	}

	public static void main(String[] args) throws Exception {
		
		pr(CljLibCore.pretty(1));
		pr(CljLibCore.pretty("x"));

		Integer x = (Integer) CljLibCore.append(-1, 1);
		pr(CljLibCore.append(x, 1));
		
		pr(CljLibCore.append(new SomeClass(), new SomeClass())); 
		
		List<Integer> v = Arrays.asList(1, 2, 3, 4, 5);
		pr(CljLibCore.prod(v));
		pr(CljLibCore.prod(BigDecimal.ONE, v));
	}
}

При загрузке класса будет автоматически инициализирован рантайм Сlojure — никаких дополнительных действий не требуется. Еще важно заметить, что мы можем расширять все протоколы напрямую из Java — нужно лишь реализовать соответствующий интерфейс. Но вот работать с объектами все равно лучше через функции, а не вызывать методы интерфейса-протокола. В противном случае не будет работать extend-protocol — очень нехорошо.

Пожалуй, главный минус этого подхода — необходимость компиляции как таковой. Еще из IDE недоступна документация для функций, нужно адаптировать исходный код библиотеки (или делать обвязку на Clojure). С другой стороны, в некоторых специфических случаях единственный вариант — иметь «честный» class-файл в classpath.

Используем clojure.lang.RT


Сердцем всего рантайма Сlojure является именно этот класс. В нем определены статические методы для создания кейвордов, символов, векторов, реализации базовых функций (например, firstи rest) и еще много чего. Класс недокументированный, не имеет постоянного интерфейса — используем на свой страх и риск. Зато там есть несколько полезных функций, делающих интеграцию предельно простой:

  • Var var(String ns, String name) — возвращает var-ячейку по полному имени;
  • Keyword keyword (String ns, String name) — возвращает keyword (первый параметр может быть null);
  • void load(String path) — загружает clj-скрипт по указанному пути.

И еще много других, проще обратится к реализации. Вызвать произвольную функцию можно так:
RT.var("clojure.core", "eval").invoke("(+ 1 2)");

Перепишем вышеприведенный пример с использованием класса RT:
import java.math.BigDecimal;

import clojure.lang.RT;
import clojure.lang.Sequential;
import clojure.lang.Var;

public class Program {

	static void pr(Object v) {
		System.out.println(v);
	}

	public static void main(String[] args) throws Exception {
		
		Var pretty = RT.var("clj-lib.core", "pretty");
		Var append = RT.var("clj-lib.core", "append");
		Var prod = RT.var("clj-lib.core", "prod");		

		pr(pretty.invoke(1));
		pr(pretty.invoke("x"));

		Integer x = (Integer) append.invoke(-1, 1);
		pr(append.invoke(x, 1));
		
		Sequential v = RT.vector(1, 2, 3, 4, 5);
		pr(prod.invoke(v));
		pr(prod.invoke(BigDecimal.ONE, v));
	}
}


Теперь мы уже не можем расширить протокол напрямую из Java — интерфейс IAppendable создается в рантайме и недоступен при компиляции нашего java-файла. Зато отпадает необходимость в AOT.

Java-интерфейс и reify


На самом деле это не отдельный способ, скорее вариант, как можно оформить взаимодействие с библиотекой. Пишем Java интерфейс:
public interface ICljLibCore {
	Number prod(Iterable x);
	Number prod(Number s, Iterable x);
	Object append(Object self, Object value);
	String pretty(Object x);
}

После этого в Clojure создаем подобную функцию:
(defn get-lib []
  (reify ICljLibCore
    (prod [_ x] (prod x))
    (prod [_ s x] (prod x))
    (append [_ t v] (append t v))
    (pretty [_ x] (pretty x))))

При обращении к библиотке мы создаем экземпляр ICljLibCore и записываем его в статическое поле:
   public static final ICljLibCore CLJ_LIB_CORE = (ICljLibCore) RT.var("clj-lib.core", "get-lib").invoke();
   ...
   CLJ_LIB_CORE.pretty("1");

Недостаток подхода — приходится вручную создавать интерфейс. С другой стороны, в этом интерфейсе можно поместить честные java-doc. Проще будет заменить Clojure библиотеку реализацией на Java (если вдруг перестанет хватать скорости), проще писать тесты. И, конечно же, никого AOT.

Заключение


За пределами статьи остались некоторые альтернативные варианты. Например, автоматически генерировать классы-обертки на основе Clojure кода и мета-данных (и оформить это в виде leiningen-плагина). Можно сделать прозрачную интеграцию в DI-фреймворк (например, Spring или Guice). Вариантов много, со своими за и против.
Tags:clojurejavainterop
Hubs: Java Lisp Clojure
+11
6.4k 46
Comments 5

Popular right now

Top of the last 24 hours