Gọi clojure từ java


165

Hầu hết các lượt truy cập hàng đầu của google cho "gọi clojure từ java" đã lỗi thời và khuyên bạn nên sử dụng clojure.lang.RTđể biên dịch mã nguồn. Bạn có thể giúp giải thích rõ ràng về cách gọi Clojure từ Java giả sử bạn đã xây dựng một jar từ dự án Clojure và đưa nó vào đường dẫn lớp không?


8
Tôi không biết rằng việc biên dịch nguồn mỗi lần là "lỗi thời" như vậy. Đó là một quyết định thiết kế. Bây giờ tôi đang làm điều đó vì nó làm cho việc tích hợp mã Clojure vào dự án Java Netbeans cũ. Thêm Clojure làm thư viện, thêm tệp nguồn Clojure, thiết lập các cuộc gọi và hỗ trợ Clojure ngay lập tức mà không cần nhiều bước biên dịch / liên kết! Với chi phí của một phần chậm trễ thứ hai trên mỗi ứng dụng bắt đầu.
Brian Knoblauch


2
Xem mới nhất cho Clojure 1.8.0 - Clojure hiện có liên kết trực tiếp với trình biên dịch.
TWR Cole

Câu trả lời:


167

Cập nhật : Vì câu trả lời này đã được đăng, một số công cụ có sẵn đã thay đổi. Sau câu trả lời ban đầu, có một bản cập nhật bao gồm thông tin về cách xây dựng ví dụ với các công cụ hiện tại.

Nó không đơn giản như biên dịch vào một jar và gọi các phương thức bên trong. Dường như có một vài thủ thuật để làm cho tất cả hoạt động mặc dù. Dưới đây là ví dụ về tệp Clojure đơn giản có thể được biên dịch sang tệp 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)))
)

Nếu bạn chạy nó, bạn sẽ thấy một cái gì đó như:

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

Và đây là một chương trình Java gọi -binomialhàm trong 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));
    }
}

Đầu ra của nó là:

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

Phần đầu tiên của phép thuật là sử dụng :methodstừ khóa trong gen-classtuyên bố. Điều đó dường như được yêu cầu để cho phép bạn truy cập vào hàm Clojure giống như các phương thức tĩnh trong Java.

Điều thứ hai là tạo ra một hàm bao bọc có thể được gọi bởi Java. Lưu ý rằng phiên bản thứ hai của -binomialcó một dấu gạch ngang phía trước nó.

Và tất nhiên, chính chiếc bình Clojure phải nằm trên đường dẫn lớp. Ví dụ này đã sử dụng bình Clojure-1.1.0.

Cập nhật : Câu trả lời này đã được kiểm tra lại bằng các công cụ sau:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • Cập nhật JDK 1.7.0 25

Phần Clojure

Đầu tiên tạo một dự án và cấu trúc thư mục liên quan bằng Leiningen:

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

Bây giờ, thay đổi thư mục dự án.

C:\projects>cd com.domain.tiny

Trong thư mục dự án, mở project.cljtệp và chỉnh sửa sao cho nội dung được hiển thị bên dưới.

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

Bây giờ, đảm bảo tất cả các phụ thuộc (Clojure) có sẵn.

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

Bạn có thể thấy một thông báo về việc tải xuống bình Clojure vào thời điểm này.

Bây giờ hãy chỉnh sửa tệp Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.cljsao cho nó chứa chương trình Clojure được hiển thị trong câu trả lời ban đầu. (Tệp này được tạo khi Leiningen tạo dự án.)

Phần lớn phép thuật ở đây là trong khai báo không gian tên. Nó báo :gen-classcho hệ thống tạo một lớp có tên com.domain.tinyvới một phương thức tĩnh duy nhất được gọi binomial, một hàm lấy hai đối số nguyên và trả về một số kép. Có hai hàm được đặt tên tương tự nhau binomial, hàm Clojure truyền thống -binomialvà trình bao bọc có thể truy cập từ Java. Lưu ý dấu gạch nối trong tên hàm -binomial. Tiền tố mặc định là một dấu gạch nối, nhưng nó có thể được thay đổi thành một cái khác nếu muốn. Các -mainchức năng chỉ làm một vài cuộc gọi đến các chức năng nhị thức để đảm bảo rằng chúng tôi đang nhận được kết quả chính xác. Để làm điều đó, biên dịch lớp và chạy chương trình.

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

Bạn sẽ thấy đầu ra được hiển thị trong câu trả lời ban đầu.

Bây giờ gói nó trong một cái lọ và đặt nó ở đâu đó thuận tiện. Sao chép bình Clojure ở đó quá.

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.

Phần Java

Leiningen có một nhiệm vụ tích hợp sẵn, lein-javaccó thể giúp với việc biên dịch Java. Thật không may, nó dường như bị hỏng trong phiên bản 2.1.3. Nó không thể tìm thấy JDK đã cài đặt và nó không thể tìm thấy kho lưu trữ Maven. Các đường dẫn đến cả hai đều có không gian nhúng trên hệ thống của tôi. Tôi cho rằng đó là vấn đề. Bất kỳ IDE Java nào cũng có thể xử lý việc biên dịch và đóng gói. Nhưng đối với bài đăng này, chúng tôi sẽ đến trường cũ và thực hiện nó theo dòng lệnh.

Đầu tiên tạo tập tin Main.javavới nội dung hiển thị trong câu trả lời ban đầu.

Để biên dịch phần java

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

Bây giờ hãy tạo một tệp có một số thông tin meta để thêm vào tệp mà chúng tôi muốn xây dựng. Trong Manifest.txt, thêm văn bản sau

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

Bây giờ gói tất cả vào một tệp jar lớn, bao gồm chương trình Clojure của chúng tôi và bình 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

Để chạy chương trình:

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

Đầu ra về cơ bản là giống hệt với sản phẩm do Clojure tạo ra, nhưng kết quả đã được chuyển đổi thành gấp đôi Java.

Như đã đề cập, một Java IDE có thể sẽ xử lý các đối số biên dịch lộn xộn và bao bì.


4
1. tôi có thể đưa ví dụ của bạn lên clojuredocs.org làm ví dụ cho macro "ns" không? 2. # ^ ở phía trước ": phương thức [# ^ {: static true} [binomial [int int] double]]" (tôi là người mới)?
Belun

5
@Belun, chắc chắn bạn có thể sử dụng nó làm ví dụ - Tôi rất hãnh diện. "# ^ {: Tĩnh true}" đính kèm một số siêu dữ liệu vào hàm cho biết nhị thức là hàm tĩnh. Nó được yêu cầu trong trường hợp này bởi vì, về phía Java, chúng ta đang gọi hàm từ hàm chính - một hàm tĩnh. Nếu nhị thức không tĩnh, việc biên dịch hàm chính ở phía Java sẽ tạo ra một thông báo lỗi về "nhị thức phương thức không tĩnh (int, int) không thể được tham chiếu từ ngữ cảnh tĩnh". Có các ví dụ bổ sung trên trang web Object Mentor.
clartaq

4
Có một điều cốt yếu không được đề cập ở đây - để biên dịch tệp Clojure sang lớp Java, bạn cần phải: (biên dịch 'com.domain.tiny)
Domchi

Làm thế nào bạn AOT biên dịch nguồn Clojure? Đây là nơi tôi bị mắc kẹt.
Matthew Boston

@MatthewBoston Vào thời điểm câu trả lời được viết, tôi đã sử dụng Enclojure, một plugin cho NetBeans IDE. Bây giờ, tôi có thể sẽ sử dụng Leiningen, nhưng chưa thử hoặc thử nghiệm nó.
clartaq

119

Kể từ Clojure 1.6.0, có một cách ưa thích mới để tải và gọi các hàm Clojure. Phương pháp này hiện được ưa thích để gọi RT trực tiếp (và thay thế nhiều câu trả lời khác ở đây). Các javadoc ở đây - điểm vào chính là clojure.java.api.Clojure.

Để tra cứu và gọi hàm Clojure:

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

Các chức năng trong clojure.coređược tự động tải. Các không gian tên khác có thể được tải thông qua yêu cầu:

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

IFns có thể được truyền cho các hàm bậc cao hơn, ví dụ ví dụ dưới đây chuyển plusđến read:

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

Hầu hết IFns trong Clojure đề cập đến các chức năng. Tuy nhiên, một số ít đề cập đến các giá trị dữ liệu phi chức năng. Để truy cập chúng, sử dụng derefthay vì fn:

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

Đôi khi (nếu sử dụng một số phần khác của thời gian chạy Clojure), bạn có thể cần đảm bảo rằng thời gian chạy Clojure được khởi tạo đúng cách - gọi một phương thức trên lớp Clojure là đủ cho mục đích này. Nếu bạn không cần gọi một phương thức trên Clojure, thì chỉ cần làm cho lớp tải là đủ (trước đây đã có một khuyến nghị tương tự để tải lớp RT; hiện tại điều này được ưu tiên):

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

1
Sử dụng phương pháp này, tập lệnh không thể sử dụng quote '() và yêu cầu mọi thứ. Có một giải pháp cho điều đó?
Renato

2
Tôi nghĩ rằng chỉ có một vài hình thức đặc biệt không tồn tại dưới dạng bình. Một cách giải quyết là truy cập nó thông qua Clojure.read("'(1 2 3"). Sẽ là hợp lý khi gửi yêu cầu này như một yêu cầu nâng cao mặc dù để cung cấp Clojure.quote () hoặc bằng cách làm cho nó hoạt động như một var.
Alex Miller

1
@Renato Không cần phải trích dẫn bất cứ điều gì, vì dù sao cũng không có gì được đánh giá bởi các quy tắc đánh giá của Clojure. Nếu bạn muốn một danh sách chứa các số 1-3, thì thay vì viết '(1 2 3)bạn hãy viết một cái gì đó như thế Clojure.var("clojure.core", "list").invoke(1,2,3). Và câu trả lời đã có một ví dụ về cách sử dụng require: nó chỉ là một var giống như bất kỳ khác.
amalloy

34

EDIT Câu trả lời này được viết vào năm 2010 và đã hoạt động vào thời điểm đó. Xem câu trả lời của Alex Miller để biết giải pháp hiện đại hơn.

Loại mã nào đang gọi từ Java? Nếu bạn có lớp được tạo với lớp gen, thì chỉ cần gọi nó. Nếu bạn muốn gọi hàm từ script, hãy xem ví dụ sau .

Nếu bạn muốn đánh giá mã từ chuỗi, bên trong Java, thì bạn có thể sử dụng mã sau:

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
Để mở rộng một chút nếu bạn muốn truy cập def'd var trong không gian tên (nghĩa là (def my-var 10)), hãy sử dụng cái này RT.var("namespace", "my-var").deref().
Ivan Koblik

Nó không hoạt động đối với tôi mà không cần thêm RT.load("clojure/core");vào lúc đầu. Những hành vi kỳ lạ?
hsestupin

Điều này hoạt động và tương tự như những gì tôi đã sử dụng; không chắc chắn nếu đó là kỹ thuật mới nhất. Tôi có thể đang sử dụng Clojure 1.4 hoặc 1.6, không chắc chắn.
Yan King Yin

12

EDIT: Tôi đã viết câu trả lời này gần ba năm trước. Trong Clojure 1.6, có một API thích hợp chính xác cho mục đích gọi Clojure từ Java. Xin vui lòng trả lời Alex Miller cho thông tin cập nhật.

Câu trả lời gốc từ năm 2011:

Như tôi thấy, cách đơn giản nhất (nếu bạn không tạo lớp với trình biên dịch AOT) là sử dụng clojure.lang.RT để truy cập các hàm trong clojure. Với nó, bạn có thể bắt chước những gì bạn đã làm trong Clojure (không cần phải biên dịch mọi thứ theo những cách đặc biệt):

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

Và trong 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);

Nó dài hơn một chút trong Java, nhưng tôi hy vọng rõ ràng rằng các đoạn mã là tương đương.

Điều này sẽ hoạt động miễn là Clojure và các tệp nguồn (hoặc các tệp được biên dịch) của mã Clojure của bạn nằm trên đường dẫn lớp.


1
Lời khuyên này đã hết hạn kể từ Clojure 1.6 - vui lòng sử dụng clojure.java.api.Clojure thay thế.
Alex Miller

Không phải là một câu trả lời tồi vào thời điểm đó, nhưng tôi muốn trả tiền cho stackoverflow.com/a/23555959/1756702 là câu trả lời có thẩm quyền cho Clojure 1.6. Sẽ thử lại vào ngày mai ...
A. Webb

Câu trả lời của Alex thực sự xứng đáng với tiền thưởng! Xin vui lòng cho tôi biết nếu tôi có thể giúp chuyển tiền thưởng nếu cần.
raek

@raek Tôi không phiền khi bạn nhận được một chút tiền thưởng từ ngón tay cò quá caffein của tôi. Hy vọng sẽ gặp lại bạn xung quanh thẻ Clojure.
A. Webb

10

Tôi đồng ý với câu trả lời của clartaq, nhưng tôi cảm thấy rằng người mới bắt đầu cũng có thể sử dụng:

  • thông tin từng bước về cách thực sự chạy này
  • thông tin hiện tại cho Clojure 1.3 và các phiên bản gần đây của leiningen.
  • một bình Clojure cũng bao gồm một chức năng chính, vì vậy nó có thể được chạy độc lập hoặc được liên kết như một thư viện.

Vì vậy, tôi bao gồm tất cả những gì trong bài viết blog này .

Mã Clojure trông như thế này:

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

Thiết lập dự án leiningen 1.7.1 trông như thế này:

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

Mã Java trông như thế này:

import ThingOne.*;

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

Hoặc bạn cũng có thể lấy tất cả mã từ dự án này trên github .


Tại sao bạn sử dụng AOT? Nó không khiến chương trình không còn độc lập với nền tảng nữa sao?
Edward

3

Điều này hoạt động với 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

Nếu trường hợp sử dụng là bao gồm một JAR được xây dựng với Clojure trong ứng dụng Java, tôi đã thấy có một không gian tên riêng cho giao diện giữa hai thế giới có lợi:

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

Không gian tên lõi có thể sử dụng thể hiện được chèn để hoàn thành các nhiệm vụ của nó:

(ns example-app.core)

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

Đối với mục đích thử nghiệm, giao diện có thể được đặt ra:

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

0

Một kỹ thuật khác cũng hoạt động với các ngôn ngữ khác trên JVM là khai báo giao diện cho các hàm bạn muốn gọi và sau đó sử dụng hàm 'proxy' để tạo cá thể thực hiện chúng.


-1

Bạn cũng có thể sử dụng trình biên dịch AOT để tạo các tệp lớp đại diện cho mã clojure của bạn. Đọc tài liệu về biên dịch, lớp gen và bạn bè trong tài liệu API Clojure để biết chi tiết về cách thực hiện việc này, nhưng về bản chất, bạn sẽ tạo một lớp gọi các hàm clojure cho mỗi lần gọi phương thức.

Một cách khác là sử dụng chức năng defprotatio và deftype mới, cũng sẽ yêu cầu biên dịch AOT nhưng cung cấp hiệu suất tốt hơn. Tôi không biết chi tiết về cách thực hiện việc này, nhưng một câu hỏi trong danh sách gửi thư có thể sẽ làm khó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.