Java Interop

Java Interop (자바 연동)

Clojure 코드를 쓸 때 한 번쯤은 꼭 자바 라이브러리를 호출해야 하는 순간이 오는데요, 그때 그냥 자바 객체를 자유롭게 다룰 수 있으면 얼마나 좋을까요? Clojure는 JVM 위에서 돌아가는 언어라서, 문법만 조금 익히면 자바의 클래스와 메서드, 필드, 배열, 원시 타입까지 그대로 다룰 수 있어요. 이 글에서는 클래스에 접근하고 멤버를 호출하는 여러 문법부터, 타입 힌트로 성능을 챙기는 방법, 그리고 자바에서 Clojure를 부르는 방법까지 Clojure와 자바 사이의 연동을 하나씩 살펴볼게요.

출처: Clojure 공식문서

본문

클래스 접근

클래스 이름을 나타내는 심볼은 해당 Class 인스턴스로 풀려요(resolve). 중첩(nested) 혹은 내부(inner) 클래스는 바깥 클래스와 $로 구분하고요, 전체 이름(fully-qualified name)은 언제나 유효해요. 클래스를 네임스페이스에 import했다면 이름을 짧게 써도 되고, java.lang의 모든 클래스는 모든 네임스페이스에 자동으로 import돼요.

네임스페이스 부분이 클래스나 원시 타입을 가리키고, 이름 부분이 1에서 9 사이의 한 자리 숫자인 심볼은 그 구성 타입(component type)과 차원을 가진 배열 클래스를 나타내요. 이 기능은 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

멤버 접근

필드나 메서드 멤버에 접근할 때 쓰는 관용적인 형태는 다음과 같아요.

(.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는 인스턴스 메서드를 가리켜요. 인스턴스 메서드를 명시하면 인스턴스는 멤버 뒤, 인자 앞에 제공돼요. qualified 인스턴스 메서드가 있으면, 인스턴스 메서드를 풀 때 인스턴스의 추가 타입 정보보다 qualified 클래스가 우선해요.

qualify되지 않은 . 형태들은 매크로 확장(macroexpansion) 시점에 dot 연산자(아래에서 설명) 호출로 확장돼요. 확장 결과는 다음과 같아요.

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

메서드 값 (Method values)

Clojure 1.12부터 qualified 메서드를 값 컨텍스트에서 보통 함수처럼 쓸 수 있어요. 컴파일러가 감싸는 함수(wrapping function)를 자동으로 생성해 주죠. 값을 쓸 때 qualified 메서드는 클래스와 메서드 이름만 제공하므로, 오버로드된 메서드를 풀 수 없어요. 그래서 오버로딩 때문에 qualified 메서드가 풀리지 않으면 컴파일러는 리플렉션 호출을 생성해요. 원하는 단일 메서드의 시그니처를 지정해 '풀어주려면' qualified 메서드에 :param-tags 메타데이터를 달면 돼요. .instanceMember 같은 unqualified 메서드에서는 :param-tags가 무시돼요.

Dot 특수 폼

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

특수 폼(special form)이에요.

. 특수 폼은 자바에 접근하는 기반이에요. 멤버 접근 연산자로 볼 수 있고, '~의 범위 안에서'라고 읽을 수도 있어요.

첫 번째 피연산자가 클래스 이름으로 풀리는 심볼이면, 그 클래스의 정적 멤버에 접근하는 것으로 간주돼요. 중첩 클래스는 JVM 명세대로 EnclosingClass$NestedClass로 이름 붙는다는 점을 기억하세요. 그 외에는 인스턴스 멤버로 간주하고 첫 번째 인자를 평가해 대상 객체를 만들죠.

클래스 인스턴스에서 인스턴스 멤버를 호출하는 특수한 경우에는, 첫 번째 인자가 클래스 인스턴스로 평가되는 표현식이어야 해요. 앞서 본 권장 형태가 Classname(identity Classname)으로 확장하는 이유가 바로 여기 있어요.

두 번째 피연산자가 심볼이고 인자가 없으면 필드 접근으로 봐요. 필드 이름은 심볼의 이름, 표현식의 값이 필드의 값이 돼요. 단, 같은 이름의 인자 없는 public 메서드가 있으면 메서드 호출로 풀리고요. 두 번째 피연산자가 -로 시작하는 심볼이면 그 멤버 심볼은 필드 접근으로만 풀려요(0-arity 메서드로는 절대 풀리지 않아요). 필드 접근이 의도라면 이 형태를 권장해요.

두 번째 피연산자가 리스트이거나 인자가 있으면 메서드 호출로 봐요. 리스트의 첫 요소는 단순 심볼이어야 하고, 메서드 이름은 그 심볼의 이름이에요. 인자가 있으면 왼쪽에서 오른쪽으로 평가되고, 일치하는 메서드로 전달돼 호출된 뒤 반환값이 돌아와요. 메서드 반환 타입이 void이면 표현식의 값은 nil이 돼요. canonic 형태에서 메서드 이름을 인자와 함께 리스트에 넣는 건 선택이지만, 그 형태 위에 만드는 매크로에서 인자를 모을 때 유용할 수 있어요.

불리언 반환값은 Boolean으로, 문자는 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이 가리키는 클래스의 생성자로 전달되고, 만들어진 객체가 반환돼요.

위에 보인 대로, canonic 특수 폼 new 외에도 Clojure는 .을 담은 심볼의 특별한 매크로 확장을 지원해요.

(new Classname args*)

(Classname. args*) ;마지막 점 주의

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

Clojure 1.12부터 qualified 형태도 쓸 수 있어요(매크로 확장 시점에 다시 쓰이지 않아요).

(Classname/new args*)

메서드와 마찬가지로 qualified 생성자 Classname/new는 값 컨텍스트에서 쓸 수 있고 :param-tags 메타데이터를 받아요.

(instance? Class expr)

expr을 평가해 그 객체가 해당 클래스의 인스턴스인지 검사하고, truefalse를 돌려줘요.

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

대입(assignment) 특수 폼이에요.

첫 피연산자가 필드 멤버 접근 형태이면 그 필드에 대입해요. 인스턴스 필드라면 instance-expr을 먼저, 그다음 expr을 평가하죠. 모든 경우에 expr의 값이 반환돼요.

주의할 점이 하나 있어요. 함수 파라미터나 로컬 바인딩에는 대입할 수 없어요. Clojure에서 가변(mutable)인 것은 자바 필드, Var, Ref, Agent뿐이에요.

(memfn method-name arg-names*)

매크로예요. 객체와 인자들을 받는 fn을 만들어 주는 코드로 확장되며, 그 객체에서 지정한 인스턴스 메서드를 인자들과 함께 호출해요. 자바 메서드를 first-class 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)

자바 객체를 받아서, 그 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 라이브러리 함수의 자바 지원

Clojure 라이브러리 함수 상당수가 자바 타입 객체에 대해 정의된 의미(semantics)를 가져요. contains?get은 자바 Map, 배열, String에 동작하고(뒤 둘은 정수 키), count는 자바 String, Collection, 배열에, nth는 자바 String, List, 배열에, seq는 자바 참조 배열, Iterable, String에 동작해요. 라이브러리 나머지 상당수가 이 함수들 위에 세워져 있기 때문에, 자바 객체를 Clojure 알고리즘에 쓰기 아주 좋은 환경이 만들어져요.

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

Clojure는 proxy 매크로로 하나 이상의 인터페이스를 구현하거나 클래스를 확장하는 객체를 동적으로 만들 수 있어요. 그 결과 객체는 익명 클래스의 인스턴스죠. 정적으로 이름 붙은 클래스와 .class 파일을 만들고 싶다면 gen-class를 쓰면 되고, 1.2부터는 인터페이스 구현에 reify도 쓸 수 있어요.

클래스, 생성자, 메서드에 자바 애너테이션을 붙일 수 있는데, gen-class와 Clojure 타입 구성에 메타데이터를 달아서 해요. 예시는 datatypes 참조 문서에서 볼 수 있어요.

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

class-and-interfaces - 클래스 이름들의 벡터
args - 슈퍼클래스 생성자에 전달할 (비어 있을 수도 있는) 인자 벡터
f => (name [params*] body) 또는 (name ([params*] body) ([params+] body) ...)

매크로예요. 주어진 fn들을 호출해 지정된 클래스/인터페이스를 구현하는 proxy 클래스의 인스턴스를 만드는 코드로 확장돼요. 클래스는 하나만 줄 수 있고, 주면 반드시 첫 번째여야 해요. 안 주면 기본값은 Object죠. 인터페이스 이름은 유효한 인터페이스 타입이어야 해요. 클래스의 메서드에 fn을 제공하지 않으면 슈퍼클래스 메서드가 호출되고, 인터페이스의 메서드에 fn을 제공하지 않으면 그 메서드가 호출될 때 UnsupportedOperationException이 던져져요. 메서드 fn은 클로저라서 proxy가 호출된 환경을 캡처할 수 있어요. 각 메서드 fn은 가려진 첫 번째 인자를 하나 더 받는데, 이 인자가 this에 바인딩돼요. protected 메서드를 오버라이드하려면 fn을 제공할 수 있지만, protected 멤버나 super에는 그 외의 접근이 없어요 — 이 능력들은 프록시할 수 없거든요.

배열

Clojure는 자바 배열의 생성, 읽기, 수정을 지원해요. 배열 사용은 배열을 인자로 요구하거나 반환값으로 쓰는 자바 라이브러리와 상호운용할 때로 제한하는 걸 권장해요.

다른 많은 Clojure 함수도 배열과 함께 동작하는데, 예를 들어 seq 라이브러리를 통해서요. 여기 나열한 함수들은 배열의 초기 생성, 또는 배열에 대한 수정·고성능 연산을 지원하기 위한 것들이에요.

가변 인자 메서드 (Vararg methods)

자바 vararg 메서드는 마지막 가변 인자 파라미터를 배열로 취급해요. Clojure에서는 varargs 자리에 명시적으로 배열을 전달해 호출할 수 있어요.

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는 타입 힌트로 성능이 민감한 코드 영역에서 컴파일러가 리플렉션을 피하도록 도와줘요. 보통은 알려진 성능 병목이 생기기 전까지 타입 힌트를 쓰지 않는 게 좋아요. 타입 힌트는 심볼이나 표현식에 붙는 메타데이터 태그로, 컴파일러가 소비해요. 함수 파라미터, 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)가 있는데, 직접 호출(direct call)로 풀 수 없을 때 컴파일러가 경고하도록 해줘요.

(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는 보통 자바 클래스 이름으로 표현되지 않는 원시 타입과 배열을 위한 별칭을 제공해요. 타입은 자바 Field 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 — object 배열

param-tags

Clojure 1.12부터 개발자는 qualified 메서드에 :param-tags 메타데이터를 넣어 원하는 단일 메서드의 시그니처를 지정하고 '풀어줄' 수 있어요. :param-tags 메타데이터는 0개 이상의 태그 벡터, 즉 [... tag ...] 형태예요. 태그는 위에서 설명한 기존의 유효한 :tag 메타데이터 값이면 뭐든 돼요. 각 태그는 원하는 시그니처의 파라미터 하나에 대응해요(arity는 태그 수와 일치해야 해요). 오버로드되지 않은 타입의 파라미터는 태그 대신 자리표시자 _를 쓸 수 있어요. qualified 메서드에 :param-tags 메타데이터를 넣으면, 컴파일러가 컴파일 시점에 그걸 단일 메서드로 풀 수 있어야 해요.

새 메타데이터 리더 문법 ^[ ... ]은 멤버 심볼에 :param-tags 메타데이터를 붙여요. ^tag가 심볼에 :tag 메타데이터를 붙이는 것과 같은 방식이죠.

자바 원시 타입 지원

Clojure는 로컬 컨텍스트에서 자바 원시 타입의 고성능 조작과 산술 연산을 지원해요. 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 등 강제(coercion) 연산은 소비자가 원시를 받을 수 있으면 원시를 만들어요.
  • num 강제 함수는 박싱해 범용 산술(generic arithmetic)을 강제해요.
  • 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 매크로.

이렇게 자바로 쓸 코드를:

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)로 함수를 흉내 내요.

Clojure 개발자는 일치하는 arity의 함수를 전달해 함수형 인터페이스를 받는 자바 메서드를 호출할 수 있어요. Clojure 컴파일러는 람다 어댑터를 만들어 함수를 필요한 함수형 인터페이스로 암시적으로 변환해요. 함수형 인터페이스로 명시적으로 강제하고 싶으면 let 바인딩에서 바인딩 이름에 힌트를 주면 되는데, 예를 들어 반복문 안에서 어댑터를 반복 생성하는 걸 피하려고 (let [^java.util.function.Predicate p even?] ...)처럼 써요.

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

몇 가지 최적화 팁

  • 모든 인자는 객체로 Clojure fn에 전달돼요. 그래서 (원시 배열 타입 힌트와 앞서 말한 long, double을 제외하면) fn 인자에 임의의 원시 타입 힌트를 붙일 의미가 없어요. 본문에서 원시 산술에 참여해야 한다면, 보여준 let 기법으로 인자를 원시 로컬에 넣어 쓰세요.
  • (let [foo (int bar)] ...)가 원시 로컬을 얻는 올바른 방법이에요. ^Integer 같은 건 쓰지 마세요.
  • 절단(truncating) 연산을 원하지 않으면 unchecked 수학을 서두르지 마세요. HotSpot은 오버플로 검사를 잘 최적화하는데, 이는 조용한 절단 대신 예외를 만들어요. 전형적인 예에서 속도 차이는 5% 정도로, 충분히 가치 있어요. 또 코드를 읽는 사람은 그 unchecked가 절단용인지 성능용인지 모르니까, 전자에만 쓰고 후자라면 주석을 다는 게 좋아요.
  • 보통 바깥 루프를 최적화할 의미가 없어요. 오히려 안 좋을 수 있는데, 안쪽 호출의 인자가 되기 위해 다시 박싱해야 하는 원시로 표현하게 되기 때문이죠. 예외는 리플렉션 경고예요 — 자주 호출되는 코드에서는 리플렉션 경고를 반드시 제거해야 해요.
  • 사실상 항상 누군가 힌트로 최적화하려는 것을 보면, 더 빠른 버전에 힌트가 훨씬 더 적어요. 힌트가 결국 개선되지 않는다면 빼세요.
  • 많은 사람들이 unchecked 연산만이 원시 산술을 한다고 생각하는데, 그렇지 않아요. 인자가 원시 로컬이면 일반 +* 등도 오버플로 검사와 함께 원시 수학을 해요 — 빠르고 안전하죠.
  • 그래서 빠른 수학의 가장 간단한 길은 연산자를 그대로 두고, 소스 리터럴과 로컬만 원시로 만드는 거예요. 원시에 대한 산술은 원시를 만들어요. 루프가 있다면(최적화하려면 보통 그럴 테니까) 루프 로컬을 먼저 원시로 만들고, 그러면 실수로 박싱된 중간 결과를 만들면 recur에서 오류가 나요. 그 오류를 중간 결과를 강제해서 풀지 말고, 어떤 인자나 로컬이 원시가 아닌지 찾아보세요.

Java Stream 지원

Clojure 컬렉션은 자바 컬렉션 인터페이스를 구현해서 StreamSpliterator 접근을 제공해요. Clojure persistent vector는 병렬 스트림을 지원하는 커스텀 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

이 연산들은 모두 종단(terminal) 스트림 연산이라 스트림을 소비해요.

간단한 XML 지원

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

(parse source)

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

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

자바에서 Clojure 호출하기

clojure.java.api 패키지는 다른 JVM 언어에서 Clojure 접근을 부트스트랩하기 위한 최소 인터페이스를 제공해요. 이를 위해 다음을 제공하죠.

  1. Clojure의 네임스페이스로 임의의 var를 찾아, 그 var의 clojure.lang.IFn 인터페이스를 돌려주는 기능.
  2. Clojure의 edn 리더로 데이터를 읽는 편의 메서드 read.

IFn은 Clojure API에 대한 완전한 접근을 제공해요. 소스나 컴파일된 형태를 classpath에 추가하기만 하면, Clojure로 작성된 다른 라이브러리에도 접근할 수 있어요.

Clojure의 public 자바 API는 다음 클래스와 인터페이스로 이뤄져요.

  • clojure.java.api.Clojure
  • clojure.lang.IFn

다른 모든 자바 클래스는 구현 세부사항으로 취급해야 하고, 애플리케이션은 이에 의존하지 않는 게 좋아요.

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은 고차 함수에 전달할 수 있어요. 예를 들어 아래 예시는 incmap에 전달해요.

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

더 알아보기