Java 연동

Java 연동 (Java Interop)

Clojure는 JVM에서 실행되므로 Java 클래스, 메서드, 필드에 직접 접근할 수 있어요. 이 페이지는 Clojure에서 Java에 접근하는 방식들을 상세히 다뤄요.

출처: Java Interop

본문

클래스 접근 (Class access)

Classname Classname$InnerClass Classname/N primitive/N

클래스 이름을 나타내는 symbol은 Class 인스턴스로 풀려요. 내부 또는 중첩 클래스는 $로 바깥 클래스와 구분돼요. 완전 한정(full-qualified) 클래스 이름은 항상 유효해요. 클래스가 네임스페이스에 import되면 한정 없이 사용할 수 있어요. java.lang의 모든 클래스는 모든 네임스페이스에 자동으로 import돼요.

ns-부분이 클래스나 프리미티브를 지칭하고 이름 부분이 1에서 9 사이의 단일 숫자인 symbol은, 그 구성요소 타입과 차원의 배열 클래스를 지정해요. 1.12에서 추가됐어요.

String
-> java.lang.String
(defn date? [d] (instance? java.util.Date d))
-> #'user/date?
(.getEnclosingClass java.util.Map$Entry)
-> java.util.Map
(.getComponentType String/1)
-> java.lang.String

멤버 접근 (Member access)

(.instanceMember instance args)* (.instanceMember Classname args)* (.-instanceField instance) (Classname/staticMethod args)* (Classname/.instanceMethod instance args)* Classname/staticField

(.toUpperCase "fred")
-> "FRED"
(.getName String)
-> "java.lang.String"
(.-x (java.awt.Point. 1 2))
-> 1
(System/getProperty "java.vm.version")
-> "1.6.0_07-b06-57"
Math/PI
-> 3.141592653589793

필드나 메서드 멤버에 접근하는 선호되는 관용적 형식은 위와 같아요. instanceMember 형식은 필드와 메서드 모두에 적용돼요. 0-인자 메서드와 필드가 둘 다 존재하면 instanceField 형식이 요구돼요.

Clojure 1.12 이후, Classname/.instanceMethod는 인스턴스 메서드를 가리켜요. 인스턴스 메서드가 지정되면 인스턴스는 멤버 다음, 인자 앞에 제공돼야 해요. 한정된 인스턴스 메서드가 있으면, 인스턴스 메서드 해석 목적에서 한정 클래스가 인스턴스에 대한 추가 타입 정보보다 우선해요.

한정 없는 "." 형식은 매크로 확장 시점에 dot 연산자(아래 서술)에 대한 호출로 확장돼요. 확장은 다음과 같아요:

(.instanceMember instance args*) ==> (. instance instanceMember args*)
(.instanceMember Classname args*) ==> (. (identity Classname) instanceMember args*)
(.-instanceField instance) ==> (. instance -instanceField)

메서드 값 (Method values)

Clojure 1.12 이후, 프로그래머는 값 컨텍스트에서 한정 메서드를 보통 함수로 사용할 수 있어요 — 컴파일러가 래핑 함수를 자동으로 생성해요. 값으로 사용될 때 한정 메서드는 클래스와 메서드 이름만 제공하므로, 오버로드된 메서드를 해석할 수 없어요. 따라서 한정 메서드가 오버로딩 때문에 해석되지 않을 때 컴파일러는 반영(reflective) 호출을 생성해요. 개발자는 한정 메서드에 :param-tags 메타데이터를 제공해 단일 원하는 메서드의 시그니처를 지정해 '해석'할 수 있어요. :param-tags.instanceMember 같은 한정 없는 메서드에서는 무시돼요.

Dot 특수 형식 (The Dot special form)

(. instance-expr member-symbol) (. Classname-symbol member-symbol) (. instance-expr -field-symbol) (. instance-expr (method-symbol args))* 또는 (. instance-expr method-symbol args)* (. Classname-symbol (method-symbol args))* 또는 (. Classname-symbol method-symbol args)*

특수 형식이에요.

'.' 특수 형식은 Java 접근의 기초예요. 멤버 접근 연산자로 간주할 수 있고/또는 '의 범위 안에서'로 읽을 수 있어요.

첫 번째 피연산자가 클래스 이름으로 풀리는 symbol이면 접근은 그 클래스의 정적 멤버로 간주돼요. 중첩 클래스는 JVM 명세대로 EnclosingClass$NestedClass로 이름 지어진다는 점에 유의해요. 그렇지 않으면 인스턴스 멤버로 간주되고 첫 번째 인자가 평가되어 대상 객체를 만들어내요.

Class 클래스 인스턴스에서 인스턴스 멤버를 호출하는 특수한 경우, 첫 번째 인자는 클래스 인스턴스로 평가되는 표현식이어야 해요 — 위의 선호 형식이 Classname(identity Classname)으로 확장한다는 것을 유의해요.

두 번째 피연산자가 symbol이고 인자가 제공되지 않으면 필드 접근으로 취급돼요 — 필드의 이름은 symbol의 이름이고, 표현식의 값은 필드의 값이에요. 같은 이름의 0-인자 public 메서드가 없을 때만 그렇고, 있으면 메서드 호출로 해석돼요. 두 번째 피연산자가 _-_로 시작하는 symbol이면 member-symbol은 오직 필드 접근으로만 해석되며(0-arity 메서드로는 절대 아님), 그런 의도일 때 선호돼야 해요.

두 번째 피연산자가 리스트이거나 인자가 제공되면 메서드 호출로 취급돼요. 리스트의 첫 요소는 단순 symbol이어야 하며, 메서드의 이름은 symbol의 이름이에요. 인자가 있으면 왼쪽에서 오른쪽으로 평가되어 매칭되는 메서드에 전달되고, 호출되어 그 값이 반환돼요. 메서드가 void 반환 타입이면 표현식의 값은 _nil_이 돼요. 리스트에 메서드 이름을 인자와 함께 두는 것은 정규 형식에서 선택적이지만, 그 형식을 기반으로 한 매크로에서 인자를 모으는 데 유용할 수 있어요.

boolean 반환 값은 Boolean으로, char는 Character로, 숫자 프리미티브는 Number로 바뀌는데, 프리미티브를 취하는 메서드가 그것을 즉시 소비하지 않는 한 그렇다는 점을 유의해요.

이 섹션의 맨 위에 주어진 멤버 접근 형식은 매크로 외의 모든 경우에 선호돼요.

(.. instance-expr member+) (.. Classname-symbol member+)

member => fieldName-symbol 또는 (instanceMethodName-symbol args*)

매크로예요. 첫 번째 인자에 대한 첫 번째 멤버의 멤버 접근(.)으로 확장되고, 이어서 결과에 대한 다음 멤버 등으로 확장돼요. 예를 들어:

(.. System (getProperties) (get "os.name"))

다음으로 확장돼요:

(. (. System (getProperties)) (get "os.name"))

하지만 쓰고, 읽고, 이해하기 쉬워요. 비슷하게 쓸 수 있는 -> 매크로도 참조해요:

(-> (System/getProperties) (.get "os.name"))

(doto instance-expr (instanceMethodName-symbol args))

매크로예요. instance-expr을 평가한 다음 모든 메서드/함수를 제공된 인자로 결과 객체에 연속해서 호출하고, 그것을 반환해요.

(doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))
-> {a=1, b=2}

(Classname. args)* (Classname/new args)* (new Classname args)*

특수 형식이에요.

인자가 있으면 왼쪽에서 오른쪽으로 평가되어 Classname이 지칭하는 클래스의 생성자에 전달돼요. 구성된 객체가 반환돼요.

보여지듯, 정규 특수 형식 new에 더해 Clojure는 '.'을 포함한 symbol의 특수 매크로 확장을 지원해요:

(new Classname args*)

다음과 같이 쓸 수 있어요:

(Classname. args*) ;뒤에 붙는 점에 주목

후자는 매크로 확장 시점에 (new Classname args) 문법으로 확장돼요.

Clojure 1.12 이후에 한정 형식도 사용할 수 있어요(매크로 확장 시점에 다시 쓰이지 않음):

(Classname/new args*)

메서드처럼, 한정 생성자 Classname/new값 컨텍스트에서 사용될 수 있고 :param-tags 메타데이터를 받아요.

(instance? Class expr)

expr을 평가하고 그것이 클래스의 인스턴스인지 검사해요. true 또는 false를 반환해요.

(set! (. instance-expr instanceFieldName-symbol) expr) (set! (. Classname-symbol staticFieldName-symbol) expr)

배정(assignment) 특수 형식이에요.

첫 번째 피연산자가 필드 멤버 접근 형식이면 대응하는 필드에 배정돼요. 인스턴스 필드라면 instance expr이 평가된 다음 expr이 평가돼요.

모든 경우 expr의 값이 반환돼요.

참고 — 함수 파라미터나 지역 바인딩에는 배정할 수 없어요. Java 필드, Var, Ref, Agent만 Clojure에서 가변적이에요.

(memfn method-name arg-names)*

매크로예요. 객체와 인자들을 받기를 기대하고, 객체에 인자들을 전달하며 이름 지어진 인스턴스 메서드를 호출하는 fn을 만드는 코드로 확장돼요. Java 메서드를 일급 fn으로 취급하고 싶을 때 사용해요.

(map (memfn charAt i) ["fred" "ethel" "lucy"] [1 2 3])
-> (\r \h \y)

이제는 거의 항상 아래 같은 문법으로 직접 하는 것이 선호된다는 점을 유의해요:

(map #(.charAt %1 %2) ["fred" "ethel" "lucy"] [1 2 3])
-> (\r \h \y)

(bean obj)

Java 객체를 받아 그 JavaBean 속성에 기반한 맵 추상화의 읽기 전용 구현을 반환해요.

(bean java.awt.Color/black)
-> {:RGB -16777216, :alpha 255, :blue 0, :class java.awt.Color,
    :colorSpace #object[java.awt.color.ICC_ColorSpace 0x5cb42b "java.awt.color.ICC_ColorSpace@5cb42b"],
    :green 0, :red 0, :transparency 1}

Clojure 라이브러리 함수에서의 Java 지원

Clojure 라이브러리 함수의 많은 것은 Java 타입의 객체에 대해 정의된 의미를 가져요. contains?와 get은 Java 맵, 배열, 문자열에서 동작하고, 후자 둘은 정수 키를 가져요. count는 Java 문자열, 컬렉션, 배열에서 동작해요. nth는 Java 문자열, 리스트, 배열에서 동작해요. seq는 Java 참조 배열, Iterable, 문자열에서 동작해요. 라이브러리의 나머지 상당 부분이 이 함수들 위에 세워져 있으므로, Clojure 알고리즘에서 Java 객체 사용에 대한 큰 지원이 있어요.

인터페이스 구현과 클래스 확장

Clojure는 하나 이상의 인터페이스를 구현하거나 클래스를 확장하는 객체의 동적 생성을 proxy 매크로로 지원해요. 결과 객체는 익명 클래스의 것이에요. gen-class로 정적으로 이름 지어진 클래스와 .class 파일을 생성할 수도 있어요. Clojure 1.2부터 인터페이스 구현을 위해 reify도 사용할 수 있어요.

Java 애노테이션은 gen-class와 Clojure 타입 구성에 대한 메타데이터를 통해 클래스, 생성자, 메서드에 붙일 수 있어요. 예는 datatypes 레퍼런스를 참조해요.

(proxy [class-and-interfaces] [args] fs+)

class-and-interfaces — 클래스 이름들의 벡터 + args — 슈퍼클래스 생성자에 대한 (어쩌면 빈) 인자 벡터 + f => (name [params*] body) 또는 (name ([params*] body) ([params+] body) ...)

매크로예요.

제공된 fn을 호출해 이름 지어진 클래스/인터페이스(들)을 구현하는 프록시 클래스의 인스턴스를 만드는 코드로 확장돼요. 단일 클래스는 제공되면 반드시 첫 번째여야 해요. 제공되지 않으면 Object로 기본값이 돼요. 인터페이스 이름은 유효한 인터페이스 타입이어야 해요. 클래스 메서드에 메서드 fn이 제공되지 않으면 슈퍼클래스 메서드가 호출돼요. 인터페이스 메서드에 메서드 fn이 제공되지 않으면 호출 시 UnsupportedOperationException이 던져져요. 메서드 fn은 클로저이며 proxy가 호출된 환경을 캡처할 수 있어요. 각 메서드 fn은 this에 바인딩되는 추가 암시적 첫 인자를 받아요. 메서드 fn을 제공해 protected 메서드를 오버라이드할 수는 있지만, 이 능력들은 프록시될 수 없으므로 protected 멤버나 super에 대한 다른 접근은 없다는 점을 유의해요.

배열 (Arrays)

Clojure는 Java 배열의 생성, 읽기, 수정을 지원해요. 배열 사용은 인자로 요구하거나 반환 값으로 사용하는 Java 라이브러리와의 연동으로 제한하는 것이 권장돼요.

많은 다른 Clojure 함수가 seq 라이브러리 같은 것을 통해 배열과 동작한다는 점을 유의해요. 여기 나열된 함수들은 배열의 초기 생성, 또는 배열에 대한 변경·고성능 연산 지원을 위해 존재해요.

Vararg 메서드

Java vararg 메서드는 꼬리 varargs 파라미터를 배열로 취급해요. Clojure에서는 vargs 대신 명시적 배열을 전달해 호출할 수 있어요.

varargs 타입에 따라 프리미티브용 타입별 배열 생성자나 특정 타입의 배열을 만드는 into-array를 사용해요. 예는 FAQ를 참조해요.

기존 컬렉션에서 배열 생성: aclone amap to-array to-array-2d into-array

다차원 배열 지원: aget aset to-array-2d make-array

타입별 배열 생성자: boolean-array byte-array char-array double-array float-array int-array long-array object-array short-array

프리미티브 배열 캐스트: booleans bytes chars doubles floats ints longs shorts

배열 변경: aset

기존 배열 처리: aget alength amap areduce

타입 힌트 (Type Hints)

Clojure는 성능-중요 코드 영역에서 반영(reflection)을 피하도록 컴파일러를 돕는 타입 힌트 사용을 지원해요. 보통은 알려진 성능 병목이 있기 전까지 타입 힌트 사용을 피해야 해요. 타입 힌트는 컴파일러가 소비하는 symbol이나 표현식에 놓이는 메타데이터 태그예요. 함수 파라미터, let-바인딩 이름, var 이름(정의 시), 표현식에 놓을 수 있어요:

(defn len [x]
  (.length x))

(defn len2 [^String x]
  (.length x))

user=> (time (reduce + (map len (repeat 1000000 "asdf"))))
"Elapsed time: 3007.198 msecs"
4000000
user=> (time (reduce + (map len2 (repeat 1000000 "asdf"))))
"Elapsed time: 308.045 msecs"
4000000

식별자나 표현식에 타입 힌트가 놓이면 컴파일러는 그에 대한 메서드 호출을 컴파일 시점에 해석하려고 시도해요. 또한 컴파일러는 반환 값의 사용을 추적하고 그 사용에 대한 타입을 추론하는 등, 완전히 컴파일-시점 해석된 호출 시리즈를 얻는 데 힌트가 아주 조금이면 충분해요. 정적 필드나 정적 메서드의 반환 값에는 컴파일러가 항상 그 타입 정보를 가지므로 타입 힌트가 필요하지 않다는 점을 유의해요.

warn-on-reflection 플래그(기본값 false)가 있는데, 컴파일러가 직접 호출을 해석할 수 없을 때 경고하게 해요:

(set! *warn-on-reflection* true)
-> true

(defn foo [s] (.charAt s 1))
-> Reflection warning, line: 2 - call to charAt can't be resolved.
-> #user/foo

(defn foo [^String s] (.charAt s 1))
-> #user/foo

함수 반환 값에 대해 타입 힌트는 파라미터 벡터 앞에 놓을 수 있어요:

(defn hinted-single ^String [])

-> #user/hinted-single

(defn hinted
  (^String [])
  (^Integer [a])
  (^java.util.List [a & args]))

-> #user/hinted

별칭 (Aliases)

Clojure는 전형적인 Java 클래스 이름 표현이 없는 프리미티브 Java 타입과 배열에 대한 별칭을 제공해요. 타입은 Java 필드 Descriptor의 명세에 따라 표현돼요. 예를 들어 byte 배열(byte-array [])은 "[B" 타입이에요.

  • int — 프리미티브 int
  • ints — int 배열
  • long — 프리미티브 long
  • longs — long 배열
  • float — 프리미티브 float
  • floats — float 배열
  • double — 프리미티브 double
  • doubles — double 배열
  • void — void 반환
  • short — 프리미티브 short
  • shorts — short 배열
  • boolean — 프리미티브 boolean
  • booleans — boolean 배열
  • byte — 프리미티브 byte
  • bytes — byte 배열
  • char — 프리미티브 character
  • chars — character 배열
  • objects — 객체 배열

param-tags

Clojure 1.12 이후, 개발자는 한정 메서드에 :param-tags 메타데이터를 제공해 단일 원하는 메서드의 시그니처를 지정해 '해석'할 수 있어요. :param-tags 메타데이터는 0개 이상의 태그의 벡터, [... tag ...]예요. 태그는 위에서 서술한 유효한 :tag 메타데이터 값이에요. 각 태그는 원하는 시그니처의 파라미터에 대응해요(arity는 태그 수와 일치해야 해요). 오버로드되지 않은 타입의 파라미터는 태그 대신 자리표시자 _를 사용할 수 있어요. 한정 메서드에 :param-tags 메타데이터를 제공하면, 그 메타데이터는 컴파일러가 컴파일 시점에 단일 메서드로 해석할 수 있게 해야 해요.

새 메타데이터 리더 문법 ^[ ... ]^tag가 symbol에 :tag 메타데이터를 붙이듯, 멤버 symbol에 :param-tags 메타데이터를 붙여요.

Java 프리미티브 지원

Clojure는 지역 컨텍스트에서 Java 프리미티브 타입의 고성능 조작과 산술을 지원해요. 모든 Java 프리미티브 타입이 지원돼요: int, float, long, double, boolean, char, short, byte.

  • let/loop-바인딩된 지역은 프리미티브 타입일 수 있으며, init-form의 추론된(아마 프리미티브) 타입을 가져요.
  • 프리미티브 지역을 다시 바인딩하는 recur 형식은 박싱 없이 하며, 같은 프리미티브 타입에 대한 타입 검사를 해요.
  • 산술(+, -, *, /, inc, dec, <, <=, >, >= 등)은 의미가 같은 곳에서 프리미티브 타입에 오버로드돼요.
  • aget / aset은 프리미티브 배열에 오버로드돼요.
  • 프리미티브 배열을 위한 aclone, alength 함수.
  • 프리미티브 배열 생성자 함수: float-array, int-array 등.
  • 프리미티브 배열을 위한 타입 힌트 — ^ints, ^floats 등.
  • 강제 변환 연산 int, float 등은 소비자가 프리미티브를 받을 수 있을 때 프리미티브를 만들어내요.
  • num 강제 변환 함수는 프리미티브를 박싱해 제네릭 산술을 강제해요.
  • 배열 캐스트 함수 ints longs 등은 int[], long[] 등을 만들어내요.
  • 최고의 성능이지만 잠재적으로 안전하지 않은 정수(int/long) 연산을 위한 "unchecked" 연산 집합: unchecked-multiply unchecked-dec unchecked-inc unchecked-negate unchecked-add unchecked-subtract unchecked-remainder unchecked-divide
  • 안전한 연산을 자동으로 unchecked 연산으로 바꾸는 동적 var: *unchecked-math*
  • 하나 이상의 배열을 함수적으로(즉 비-파괴적으로) 처리해 각각 새 배열이나 집계 값을 만들기 위한 amapareduce 매크로.

이 Java를 쓰는 대신:

static public float asum(float[] xs){
  float ret = 0;
  for(int i = 0; i < xs.length; i++)
    ret += xs[i];
  return ret;
}

이 Clojure를 쓸 수 있어요:

(defn asum [^floats xs]
  (areduce xs i ret (float 0)
    (+ ret (aget xs i))))

결과 코드는 (java -server로 실행할 때) 정확히 같은 속도예요.

이것의 가장 좋은 점은 초기 코딩에서 특별한 일을 할 필요가 없다는 것이에요. 꽤 자주 이 최적화는 불필요해요. 코드 조각이 병목이라면 약간의 장식으로 속도를 높일 수 있어요:

(defn foo [n]
  (loop [i 0]
    (if (< i n)
      (recur (inc i))
      i)))

(time (foo 100000))
"Elapsed time: 0.391 msecs"
100000

(defn foo2 [n]
  (let [n (int n)]
    (loop [i (int 0)]
      (if (< i n)
        (recur (inc i))
        i))))

(time (foo2 100000))
"Elapsed time: 0.084 msecs"
100000

함수는 프리미티브 인자와 반환 타입에 대한 제한된 지원을 가져요: longdouble(오직 이것들)에 대한 타입 힌트는 프리미티브-타입 오버로드를 생성해요. 이 능력은 arity가 4 이하인 함수로 제한된다는 점을 유의해요.

따라서 다음과 같이 정의된 함수는

(defn foo ^long [^long n])

프리미티브 타입 long의 값을 받기도 하고 반환하기도 해요(박싱된 인자로 호출하는 것은 물론 어떤 객체 결과든 프리미티브-타입 오버로드에 대한 캐스트와 위임을 초래해요).

강제 변환 (Coercions)

프리미티브 강제 변환

특정 프리미티브 타입의 값이 필요한 때가 있어요. 이 강제 변환 함수들은 그러한 강제 변환이 가능한 한 표시된 타입의 값을 산출해요: bigdec bigint boolean byte char double float int long num short

함수형 인터페이스 변환 (Functional Interface Conversion)

Java 프로그램은 단일 메서드를 가진 함수형 인터페이스(Functional Interfaces)로 함수를 흉내 내요.

Clojure 개발자는 일치하는 arity의 함수를 전달해 함수형 인터페이스를 취하는 Java 메서드를 호출할 수 있어요. Clojure 컴파일러는 람다 어댑터를 구성함으로써 함수를 요구된 함수형 인터페이스로 암시적으로 변환해요. 루프에서 반복 어댑터 구성을 피하려면 let 바인딩에서 바인딩 이름을 힌트함으로써 함수를 함수형 인터페이스로 명시적으로 coerce할 수 있어요, 예: (let [^java.util.function.Predicate p even?] ...).

Clojure 1.12 이후, 모든 IDeref 구현(delay, future, atom 등)은 Supplier 인터페이스를 직접 구현해요.

최적화 팁 몇 가지

  • 모든 인자는 Clojure fn에 객체로 전달되므로, fn 인자(프리미티브 배열 타입 힌트와 long·double은 제외)에 임의의 프리미티브 타입 힌트를 놓을 의미가 없어요. 대신, 본문에서 프리미티브 산술에 참여해야 한다면 프리미티브 지역에 인자를 두는 let 테크닉을 사용해요.
  • (let [foo (int bar)] ...)가 프리미티브 지역을 얻는 올바른 방법이에요. ^Integer 등을 사용하지 마세요.
  • 잘림(truncation) 연산을 원하지 않으면 unchecked 수학을 서두르지 마세요. HotSpot이 오버플로 검사 최적화를 잘하며, 이는 조용한 잘림 대신 예외를 산출해요. 전형적 예에서 속도 차이가 약 5% 정도로, 충분히 가치가 있어요. 또한 코드를 읽는 사람들이 unchecked를 잘림 목적으로 쓰는지 성능 목적으로 쓰는지 모르므로 — 전자에 예약하고 후자라면 주석을 다는 것이 좋아요.
  • 바깥 루프를 최적화하려는 것은 보통 의미가 없어요. 실제로 내부 호출의 인자가 되기 위해 다시 박싱돼야 하는 프리미티브로 나타내게 되므로 해가 될 수 있어요. 유일한 예외는 반영 경고(reflection warnings)로, 자주 호출되는 어떤 코드에서도 제거해야 해요.
  • 누군가 힌트로 최적화하려는 것을 제시할 때마다 거의 항상 더 빠른 버전이 원본보다 힌트가 훨씬 적어요. 힌트가 결국 개선하지 못하면 뺴내세요.
  • 많은 사람이 프리미티브 산술을 오직 unchecked- 연산만 한다고 가정해요 — 그렇지 않아요. 인자가 프리미티브 지역일 때 일반 +와 * 등은 오버플로 검사로 프리미티브 수학을 하며 — 빠르고 안전해요.
  • 따라서 빠른 수학의 가장 단순한 길은 연산자를 그대로 두고 소스 리터럴과 지역이 프리미티브인지만 확인하는 것이에요. 프리미티브에 대한 산술은 프리미티브를 산출해요. 루프가 있다면(최적화가 필요하다면 아마 루프가 있을 것) 루프 지역이 먼저 프리미티브인지 확인하고 — 실수로 박싱된 중간 결과를 만들면 recur에서 오류를 얻게 돼요. 그 오류를 중간 결과를 coerce해서 해결하지 말고, 어떤 인자나 지역이 프리미티브가 아닌지 알아내세요.

Java 스트림 지원

Clojure 컬렉션은 Java 컬렉션 인터페이스를 구현하며, 이것들은 StreamSpliterator 접근을 제공해요. Clojure 지속 벡터는 병렬 스트림을 지원하는 사용자 정의 Spliterator를 구현해요.

Clojure는 (1.12 이후) 스트림과 관용적으로 상호 운용하는 함수를 제공하며, 모든 함수는 Clojure 대응물과 유사하게 동작해요:

  • (stream-seq! stream) => seq
  • (stream-reduce! f [init-val] stream) => val
  • (stream-transduce! xf f [init-val] stream) => val
  • (stream-into! to-coll [xf] stream) => to-coll

이 모든 연산은 말단 스트림 연산(스트림을 소비)이에요.

간단한 XML 지원

배포판에 간단한 XML 지원이 포함돼 있으며, src/clj/clojure/xml.clj 파일에 있어요. 이 파일의 모든 이름은 clojure.xml 네임스페이스에 있어요.

(parse source)

File, InputStream 또는 URI를 지칭하는 String일 수 있는 소스를 파싱하고 로드해요. :tag, :attrs, :content 키와 tag, attrs, content 접근자 fn을 가진 clojure.xml/element struct-map의 트리를 반환해요.

(clojure.xml/parse "/Users/rich/dev/clojure/build.xml")
-> {:tag :project, :attrs {:name "clojure", :default "jar"}, :content [{:tag :description, ...

Java에서 Clojure 호출하기

clojure.java.api 패키지는 다른 JVM 언어에서 Clojure 접근을 부트스트랩하는 최소 인터페이스를 제공해요. 다음을 제공함으로써 그렇게 해요:

  1. Clojure의 네임스페이스를 사용해 임의의 var를 위치시키고, var의 clojure.lang.IFn 인터페이스를 반환하는 능력.
  2. Clojure의 edn 리더로 데이터를 읽기 위한 편의 메서드 read.

IFn은 Clojure의 API에 대한 완전한 접근을 제공해요. 소스든 컴파일된 형태든 클래스패스에 추가한 후에는 Clojure로 쓰인 다른 어떤 라이브러리에도 접근할 수 있어요.

Clojure의 공개 Java API는 다음 클래스와 인터페이스로 구성돼요:

그 외 모든 Java 클래스는 구현 세부 사항으로 취급되어야 하며, 애플리케이션은 그것들에 의존하지 말아야 해요.

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

IFn은 고차 함수에 전달할 수 있어요, 예: 아래 예는 mapinc를 전달해요:

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

Clojure의 대부분의 IFn은 함수를 가리켜요. 그러나 일부는 비-함수 데이터 값을 가리켜요. 이들에 접근하려면 함수를 호출하는 대신 deref를 사용해요:

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

더 알아보기