Chamando clojure de java

165

A maioria dos principais hits do google para "chamar clojure de java" está desatualizada e recomenda o uso clojure.lang.RTpara compilar o código-fonte. Você poderia ajudar com uma explicação clara de como chamar o Clojure a partir do Java, assumindo que você já criou um jar a partir do projeto Clojure e o incluiu no caminho de classe?

Arthur Ulfeldt
fonte
8
Não sei se a compilação da fonte sempre está "desatualizada". É uma decisão de design. Estou fazendo isso agora, pois facilita a integração do código Clojure em um projeto Java Netbeans legado. Adicione o Clojure como uma biblioteca, adicione um arquivo de origem do Clojure, configure as chamadas e suporte instantâneo ao Clojure sem ter várias etapas de compilação / vinculação! Com o custo de uma fração de segundo de atraso em cada aplicativo iniciado.
precisa
2
Veja a versão mais recente do Clojure 1.8.0 - O Clojure agora possui link direto do compilador.
TWR Cole

Respostas:

167

Atualização : desde que esta resposta foi publicada, algumas das ferramentas disponíveis foram alteradas. Após a resposta original, há uma atualização incluindo informações sobre como criar o exemplo com as ferramentas atuais.

Não é tão simples como compilar em um jar e chamar os métodos internos. Parece haver alguns truques para fazer tudo funcionar. Aqui está um exemplo de um arquivo Clojure simples que pode ser compilado em um 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)))
)

Se você executá-lo, verá algo como:

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

E aqui está um programa Java que chama a -binomialfunção no 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));
    }
}

Sua saída é:

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

A primeira peça de mágica está usando a :methodspalavra - chave na gen-classdeclaração. Isso parece ser necessário para permitir que você acesse a função Clojure, como métodos estáticos em Java.

A segunda coisa é criar uma função de invólucro que pode ser chamada pelo Java. Observe que a segunda versão -binomialtem um traço na frente.

E, é claro, o próprio jar Clojure deve estar no caminho da classe. Este exemplo usou o jar Clojure-1.1.0.

Atualização : Esta resposta foi testada novamente usando as seguintes ferramentas:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Atualização 25

A parte de Clojure

Primeiro, crie um projeto e uma estrutura de diretórios associada usando Leiningen:

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

Agora, mude para o diretório do projeto.

C:\projects>cd com.domain.tiny

No diretório do projeto, abra o project.cljarquivo e edite-o para que o conteúdo seja como mostrado abaixo.

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

Agora, verifique se todas as dependências (Clojure) estão disponíveis.

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

Você pode ver uma mensagem sobre o download do jar Clojure neste momento.

Agora edite o arquivo Clojure para C:\projects\com.domain.tiny\src\com\domain\tiny.cljque ele contenha o programa Clojure mostrado na resposta original. (Este arquivo foi criado quando Leiningen criou o projeto.)

Grande parte da mágica aqui está na declaração do espaço para nome. O comando :gen-classdiz ao sistema para criar uma classe nomeada com.domain.tinycom um único método estático chamado binomial, uma função que recebe dois argumentos inteiros e retorna um duplo. Existem duas funções com nomes semelhantes binomial, uma função tradicional do Clojure -binomiale um wrapper acessível a partir de Java. Observe o hífen no nome da função -binomial. O prefixo padrão é um hífen, mas pode ser alterado para outra coisa, se desejado. A -mainfunção apenas faz algumas chamadas para a função binomial para garantir que estamos obtendo os resultados corretos. Para fazer isso, compile a classe e execute o programa.

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

Você deve ver a saída mostrada na resposta original.

Agora, embrulhe-o em uma jarra e coloque-o em algum lugar conveniente. Copie o frasco Clojure também.

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.

A parte Java

Leiningen tem uma tarefa interna,, lein-javacque deve poder ajudar na compilação do Java. Infelizmente, parece estar quebrado na versão 2.1.3. Ele não consegue encontrar o JDK instalado e o repositório Maven. Os caminhos para ambos têm espaços incorporados no meu sistema. Presumo que esse seja o problema. Qualquer IDE Java também pode lidar com a compilação e o empacotamento. Mas para este post, estamos indo para a velha escola e fazendo isso na linha de comando.

Primeiro, crie o arquivo Main.javacom o conteúdo mostrado na resposta original.

Para compilar parte java

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

Agora crie um arquivo com algumas meta-informações para adicionar ao jar que queremos construir. Em Manifest.txt, adicione o seguinte texto

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

Agora empacote tudo em um arquivo jar grande, incluindo nosso programa Clojure e o 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

Para executar o programa:

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

A saída é essencialmente idêntica à produzida apenas por Clojure, mas o resultado foi convertido em um Java duplo.

Como mencionado, um Java IDE provavelmente cuidará dos argumentos confusos de compilação e do empacotamento.

clartaq
fonte
4
1. posso colocar seu exemplo no clojuredocs.org como exemplo para a macro "ns"? 2. o que está # ^ na frente de ": methods [# ^ {: static true} [binomial [int int] double]]" (eu sou um novato)?
Belun
5
@ Belun, com certeza você pode usá-lo como exemplo - estou lisonjeado. O "# ^ {: static true}" anexa alguns metadados à função indicando que o binômio é uma função estática. É necessário neste caso, porque, no lado Java, estamos chamando a função de main - uma função estática. Se o binomial não fosse estático, a compilação da função principal no lado Java produziria uma mensagem de erro sobre "método não estático binomial (int, int) não pode ser referenciado a partir de um contexto estático". Existem exemplos adicionais no site do Object Mentor.
clartaq
4
Há uma coisa crucial não mencionado aqui - para compilar arquivo Clojure a classe Java, você precisa: (compilação 'com.domain.tiny)
Domchi
como você está compilando a fonte Clojure? É aqui que estou preso.
Matthew Boston
@MatthewBoston Na época em que a resposta foi escrita, eu usei o Enclojure, um plug-in para o NetBeans IDE. Agora, eu provavelmente usaria Leiningen, mas ainda não o testou.
clartaq
119

A partir do Clojure 1.6.0, há uma nova maneira preferida de carregar e chamar funções do Clojure. Agora, esse método é preferido para chamar diretamente o RT (e substitui muitas das outras respostas aqui). O javadoc está aqui - o principal ponto de entrada é clojure.java.api.Clojure.

Para procurar e chamar uma função Clojure:

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

As funções clojure.coresão carregadas automaticamente. Outros namespaces podem ser carregados via exigem:

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

IFns podem ser passados ​​para funções de ordem superior, por exemplo, o exemplo abaixo passa pluspara read:

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

A maioria dos IFns no Clojure refere-se a funções. Alguns, no entanto, referem-se a valores de dados não funcionais. Para acessá-los, use em derefvez de fn:

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

Às vezes (se você estiver usando outra parte do tempo de execução do Clojure), pode ser necessário garantir que o tempo de execução do Clojure seja inicializado corretamente - chamar um método na classe Clojure é suficiente para esse fim. Se você não precisar chamar um método no Clojure, basta fazer com que a classe carregue é suficiente (no passado, havia uma recomendação semelhante para carregar a classe RT; isso agora é preferido):

Class.forName("clojure.java.api.Clojure") 
Alex Miller
fonte
1
Usando essa abordagem, o script não pode usar quote '() e exigir coisas. Existe uma solução para isso?
Renato
2
Eu acho que existem apenas algumas formas especiais que também não existem como vars. Uma solução alternativa é acessá-lo via Clojure.read("'(1 2 3"). Seria razoável arquivar isso como uma solicitação de aprimoramento para fornecer um Clojure.quote () ou fazê-lo funcionar como uma var.
Alex Miller
1
@Renato Não há necessidade de citar nada, porque nada está sendo avaliado pelas regras de avaliação da Clojure. Se você quiser uma lista contendo os números 1-3, em vez de escrever, '(1 2 3)escreva algo parecido Clojure.var("clojure.core", "list").invoke(1,2,3). E a resposta já tem um exemplo de como usar require: é apenas uma var como qualquer outra.
precisa saber é
34

EDITAR Esta resposta foi escrita em 2010 e funcionou na época. Veja a resposta de Alex Miller para uma solução mais moderna.

Que tipo de código está chamando de Java? Se você tem classe gerada com gen-class, basta chamá-lo. Se você deseja chamar a função a partir do script, veja o exemplo a seguir .

Se você deseja avaliar o código da string, dentro de Java, é possível usar o seguinte código:

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
fonte
1
Para expandir um pouco, se você quiser acessar def'd var no espaço para nome (ou seja (def my-var 10)), use-o RT.var("namespace", "my-var").deref().
perfil completo de Ivan Koblik
Não funciona para mim sem adicionar RT.load("clojure/core");no início. Qual o comportamento estranho?
usar o seguinte comando
Isso funciona e é semelhante ao que eu tenho usado; não tenho certeza se é a mais nova técnica. Posso estar usando o Clojure 1.4 ou 1.6, não tenho certeza.
Yan King Yin
12

Edição: Eu escrevi esta resposta há quase três anos. No Clojure 1.6, existe uma API adequada exatamente com o objetivo de chamar o Clojure a partir de Java. Por favor , responda a resposta de Alex Miller para obter informações atualizadas.

Resposta original de 2011:

A meu ver, a maneira mais simples (se você não gerar uma classe com a compilação AOT) é usar o clojure.lang.RT para acessar funções no clojure. Com ele, você pode imitar o que você faria no Clojure (não é necessário compilar as coisas de maneiras especiais):

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

E em 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);

É um pouco mais detalhado em Java, mas espero que fique claro que as partes do código são equivalentes.

Isso deve funcionar desde que o Clojure e os arquivos de origem (ou arquivos compilados) do seu código Clojure estejam no caminho de classe.

raek
fonte
1
Este conselho está desatualizado no Clojure 1.6 - use clojure.java.api.Clojure.
Alex Miller
Não era uma resposta ruim na época, mas pretendia recompensar o stackoverflow.com/a/23555959/1756702 como a resposta oficial do Clojure 1.6. Tentarei novamente amanhã ...
A. Webb
A resposta de Alex realmente merece a recompensa! Informe-me se posso ajudar na transferência da recompensa, se necessário.
raek
@raek Não me importo que você tenha recebido um pequeno bônus do meu dedo em excesso de cafeína. Espero vê-lo novamente na tag Clojure.
A. Webb
10

Concordo com a resposta da clartaq, mas achei que os iniciantes também poderiam usar:

  • informações passo a passo sobre como realmente fazer isso funcionar
  • informações atualizadas para o Clojure 1.3 e versões recentes do leiningen.
  • um jar Clojure que também inclui uma função principal, para que possa ser executado de forma independente ou vinculado como uma biblioteca.

Então, eu cobri tudo isso nesta postagem do blog .

O código Clojure é assim:

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

A configuração do projeto leiningen 1.7.1 é assim:

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

O código Java fica assim:

import ThingOne.*;

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

Ou você também pode obter todo o código deste projeto no github .

sandover
fonte
Por que você usou o AOT? Não faz com que o programa não seja mais independente de plataforma?
Edward
3

Isso funciona com o 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
fonte
2

Se o caso de uso for incluir um JAR criado com o Clojure em um aplicativo Java, descobri que ter um espaço para nome separado para a interface entre os dois mundos é benéfico:

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

O namespace principal pode usar a instância injetada para realizar suas tarefas:

(ns example-app.core)

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

Para fins de teste, a interface pode ser stubbed:

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

Outra técnica que também funciona com outros idiomas na JVM é declarar uma interface para funções que você deseja chamar e, em seguida, usar a função 'proxy' para criar uma instância que as implemente.

Petr Gladkikh
fonte
-1

Você também pode usar a compilação AOT para criar arquivos de classe que representam seu código de clojure. Leia a documentação sobre compilação, classe gen e amigos nos documentos da API Clojure para obter detalhes sobre como fazer isso, mas, em essência, você criará uma classe que chama funções de clojure para cada chamada de método.

Outra alternativa é usar a nova funcionalidade defprotocol e deftype, que também exigirá compilação AOT, mas proporcionará melhor desempenho. Ainda não sei os detalhes de como fazer isso, mas uma pergunta na lista de discussão provavelmente faria o truque.

rosejn
fonte