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

    Язык 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). Вариантов много, со своими за и против.

    Комментарии 5

      0
      А можно еще пару примеров задач, в которой такой подход удобнее чем изначальная реализация на Java? Или это только случаи когда код на Clojure уже есть и работает и надо его переиспользовать?

      Какие преимущества дает Clojure?
        0
        Описание преимущества Clojure перед Java будет по объему больше этой статьи. :)
        Такой подход скорее нужен, если уже есть проект на Java, и возникло желание написать некую подсистему на Clojure.
        Причинами могут быть, например, желание использовать DSL в своем проекте (макросы). Могут быть полезны персистентные коллекции и STM, агенты. Можно использовать Clojure как скриптовый язык (через eval). А можно просто любить программировать на Clojure — язык очень хорош :)
          0
          Может вы бы лучше тогда как раз статью об этих преимуществах написали? Периодически честно пытаюсь усомниться в том что (не)популярность этого языка обусловлена его непрактичностью, а не тем что миллионы программистов просто не хотят учиться ничему новому (что само по себе звучит как самоисключающее утверждение), и пытаюсь найти примеры того в каких же случаях clojure действительно удобнее тех же (раз уж разговор про Java) Scala или Groovy. Очень хотелось бы понять в чём преимущества не только функциональных возможностей (которые есть и в Scala и в Groovy), а именно всего остального из clojure, чего в распространённых языках не наблюдается. Иногда слышатся восклицания что макросы в clojure каким-то образом могут сохранить очень много времени и сил, но вот примеров нормальных что-то нет.
          В общем, если бы кто-то написал такую статью, или может даже цикл статей, цены бы им не было, информации по этому мало. Именно clojure такие статьи как раз очень нужны. К примеру в Groovy доки смотришь и сразу понимаешь насколько всё это удобно и где может использоваться. А от clojure документации толку особо нету…
            0
            Ваша правда. Хотя, думаю, большую часть людей отпугивают скобочки. Меня отпугивали. Собственно, я и Clojure начал изучать уже после того, как познакомился поглубже с Groovy и разочаровался в этом языке.
            А статью о преимуществах (статьи?) я и сам подумывал написать. Видимо стоит. :)
              0
              не знаю видели ли вы уже, но похоже вас услышали прям в ИБМ и сделали аккурат что вы хотели — перевели серию статей о языках Java.next :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое