- Learn about Wiki
- Lectures
- Study
- Tips
클로저의 코드는 클로저의 데이타로 구성된다. 클로저 코드가 실행될 때 클로저의 reader는 코드가 작성된 텍스트를 읽고, 클로져 데이타 구조로 변환한다. 그런 다음, 클로저 런타임이 그 데이타구조를 컴파일하고 실행한다.
그러나 보통의 텍스트가 바로 클로저 데이타로 사용될 수는 없다. 클로져의 reader는 텍스트를 읽어 클로저 데이타로 바꾸는 작업을 한다.
클로저 reader는 read 함수를 사용한다.
(read) ;<< ( +, 1, 1) ;=> (+ 1 1)
클로져 reader에서 리턴된 리스트 문구(form)는 바로 평가될 수 있다.
(eval (read)) ;<< (+ 1 1) ;=> 2
pr은 클로저 데이타 구조를 프린트한다.
(pr (eval (read))) ;<< (+ 1 1) ;>> 6 ;=> nil
위 코드를 loop를 돌리면 간단한 대화형 쉘이 된다.
(loop [] (pr (eval (read))) (recur))
REPL은 파이썬이나 루비등 주로 동적 언어에서 제공되는 프로그래밍을 위한 대화형 쉘이다. 이것은 Read-Evaluate-Print-Loop의 첫 문자를 딴 단어인데, 코드를 읽고 평가하고 결과를 프린트하는 루프를 도는 과정을 통해서 프로그래머들이 대화형으로 코드를 테스트해 볼 수 있는 환경을 제공한다.
클로저의 REPL은 단순히 인터프리터가 아니라, 입력된 코드가 JVM 바이트코드로 컴파일되어 바로 실행되는데, 이것은 소스 파일에서 직접읽어 실행하는 것과 하등 차이가 없다.
다음과 같이 REPL을 실행시켜 볼 수 있다.
% java -cp clojure-1.4.0.jar clojure.main Clojure 1.4.0 user=>
혹은
% java -jar clojure-1.4.0.jar Clojure 1.4.0 user=>
이렇게 하면 하나의 JVM 프로세스가 시작되고, 프롬프트가 보여주고 코드 입력을 대기한다. 프롬프트는 현재의 이름 공간을 표시해 주는데, 현재 이름 공간은 user이다. REPL은 상당히 강력하고 빠른 프로그램을 개발할 수 있는 환경을 제공해 준다.
클로저 소스 파일의 확장자는 .clj이다. 다음과 같이 에디터를 열고 hello.clj 파일을 편집한다.
(def message "Hello, world!") (println message)
이 파일을 실행하는 방법은 두 가지다.
REPL에서 실행하기 위해서는 REPL을 구동한 다음, 다음과 같이 한다.
(load-file "hello.clj") >> Hello, world! >> nil message >> Hello, world!
load-file은 로드할 .clj 파일의 경로를 입력받아 파일을 REPL에 로드한다. 로드하면서 마치 REPL에서 직접 입력한 것 처럼 파일의 각 문구들을 순서대로 실행한다. 정의된 모든 심볼들은 계속 유효하다.
명령창을 열고 다음과 같이 입력한다.
> java -jar clojure-1.4.0.jar hello.clj Hello, world
이렇게 하면 새로운 JVM이 실행되면서 클로저 런타임이 구동된다. 클로저 런타임은 hello.clj를 로드하고 실행한다. 이것은 자바 파일을 실행하는 것과 아주 비슷하다.
코드를 바로 실행해 볼 수도 있다.
> java -jar clojure-1.4.0.jar -e "(+ 3 4)" 7
스트링은 이중 따옴표로 둘러싸여 표기된다.
"hello world!" ;=> "hello world!"
다중 라인이 자동으로 된다.
"multiline strings are very handy" ;=> "multiline strings\nare very handy"
클로져의 스트링은 자바 스트링이다(즉 java.lang.String의 인스턴스이다).
(class "hello") ;=> java.lang.String
문자는 역슬래쉬로 표기된다.
(class \c) ;=> java.lang.Character
유니코드와 8진수 표현이 가능하다.
\u00ff ;=> \ÿ \o41 ;=> \!
true는 참이고, false는 거짓이다. nil은 false는 다르지만, 참과 거짓을 판단하는 상황에서는 false로 평가된다.
(= nil false) ;= false (if false :a :b) ;= :b (if nil :a :b) ;= :b
빈 리스트 ()와 0은 false가 아니다.
(if () :a :b) ;= :a (if 0 :a :b) ;= :a
nil 은 자바의 null에 해당한다. 또한 논리적으로 false와 같다. 리스프와는 다르게 nil은 ()이 아니다.
(= nil ()) ;>> false
키워드는 자기자신으로 평가된다. 클로져에서는 맵이나 레코드에서 값에 대한 접근자로서 사용된다.
(def person {:name "Hong Kil-Dong" :age 23}) ;=> #'user/person (:age person) ;=> 23
키워드는
:이 앞에 붙으며 공백문자를 제외한 모든 문자로 구성된다.
/는 이름공간을 구분해 준다.
::는 현재의 이름공간내의 키워드를 의미한다.
(def person {:name "Hong Kil-Dong" :location "west" ::location "east"}) ;=> #'user/person person ;=> {:name "Hong Kil-Dong" :location "west" :user/location "east"} (:user/location person) ;=> "east"
XML처럼 키워드가 이름공간 속에 있어서, 충돌을 방지하기 위한 밑줄이나 복잡한 도메인 모델링없이, 한 어플리케이션의 여러 모듈에서 혹은 같은 조직내에서의 별개의 그룹에서 특정 이름을 안전하게 가리킬 수 있게 된다.
name 함수로 키워드의 이름을 스트링으로 얻을 수 있으며, namespace 함수로 이름공간의 이름을 얻을 수 있다.
(name :user/location) ;=> "location" (namespace :user/location) ;=> "user" (namespace :location) ;=> nil
키워드와 비슷하지만, 심볼은 자기자신이 아닌 다른 값으로 평가된다. 즉 자기자신이 아닌 vars의 값이나 자바 클래스, 지역 변수 등등이다.
(def my-add (fn [x y] (+ x y)))
여기서 my-add는 var 안의 함수를 가리키는 심볼이다.
심볼은 숫자 아닌 문자로 시작해야 하며, * + ! - _ ? 등이 문자뒤에 붙을 수 있다. /이 붙으면 심볼이 속해 있는 이름공간을 나타낸다.
def 가 함수를 가르치는 경우 아래와 같이 defn 으로 약식 표기할 수 있다.
(defn my-add [x y] (+ x y))
클로져는 다양한 수 표현 능력이 있다.
리터럴 문법 | 수 형식 |
---|---|
42, 0xff, 2r111, 040 | long(64-bit signed integer) |
3.14, 6.0221415e23 | double(64-bit IEEE floating point decimal) |
42N | clojure.lang.BigInt(arbitrary-precision integer) |
0.01M | java.math.BigDecimal(arbitrary-precision signed floating point decimal) |
22/7 | clojure.lang.Ratio |
clojure.lang.BigInt는 필요시 자동으로 java.math.BigInteger로 변환된다.
0xff와 같은 16진수 표기, 040 과 같은 8진수 표기와 더불어, 임의의 진수 표기가 가능하다. 2r111은 2진수 111을 나타내며, 16rff는 16진수 16을, 5r34는 5진수 34를 나타낸다.
N과 M을 숫자 뒤에 붙여서 임의 정밀도 수를 표기할 수 있으며, 분수 표기도 가능하다.
클로저에서 정수는 이론적으로는 무한수까지 가능하지만, 시스템이 제공하는 메모리 크기에 제한된다.
42 +9 -107 99477470020757020748759207629460386871485960847968718485528475260847692104857254876204957284573785810938
다음은 진법 표기이다.
[127 0x7F 0177 16r7F 32r3V 2r01111111] ;>> [127 127 127 127 127 127]
진수(radix) 표기 방식인 16r7F는 0x7F와 같은 16진법 표기이다. 진수(radix)를 'r' 앞에 붙이고 뒤에 수를 쓴다. 진수(radix) 표기는 최대 36까지이다. 32r3V는 32진수이고 3과 V는 가갂 10진수 3과 31이다.
1.17 +1.22 -2. 366e7 32e-14 10.7e-3
클로저에서는 분수 표현이 가능하다.
22/7 7/22 7478174915757250485028195/374974857577947294847239 -103/4
분수가 딱 떨어질 때는 정수로 바뀐다. 가능다하면 약수분해를 한다.
10/2 ;>> 5 10/4 ;>> 5/2
문자 스트링앞에 #을 쓰면 정규표현식이 된다.
(class #"(p|h)ail") ;=> java.util.regex.Pattern (re-seq #"(...) (...)" "foo bar") ;=> (["foo bar" "foo" "bar"])
한편으로 정규식이 하나의 리터럴로 된다는 것이 특이한데, 이로 인해 편한 점은 자바에서처럼 \를 2번 쓰지 않아도 된다는 것이다.
(re-seq #"(\d+)-(\d+)" "1-3") ;; 자바에서는 "(\\d+)-(\\d+)" 와 같이 된다. ;=> (["1-3" "1" "3'])
한줄 코멘트는 ;를 사용한다.
(defn sum [& r] (apply + r)) ; definition of sum ;=> 'user/sum
문구 코멘트는 리더 매크로 #_를 사용한다.
(+ 1 2 #_(* 2 2) 8) ;=> 11
클로져는 항상 괄호가 쓰이기 때문에 문구 코멘트는 매우 유용하다.
(defn some-function [...arguments...] ...code... (if ...debug-conditional... (println ...debug-info...) (println ...more-debug-info...) ...code...)
위 코드에서 if 문구을 #_ 으로 코멘트 처리하면 해당 코드 전체가 코멘트된다.
comment 매크로는 코멘트 처리에 사용될 수 있으나 항상 nil을 반환한다.
(+ 1 2 (comment (* 2 2)) 8) ;=> #<NullPointerException java.lang.NullPointerException>
콤마는 리더에 의해 공백문자로 취급된다.
(= [1 2 3] [1, 2, 3]) ;=> true
콤마를 사용해서 더 읽기 좋아질 때만 사용하는 것이 좋다.
(create-user {:name new-username, :email email})
다음은 클로져 자료구조 list, vector, map, set에 대한 리터럴들이다.
'(a b :name 12.5) ;; list ['a 'b :name 12.5] ;; vector {:name "Hong" :age 23} ;; map #{1 2 3} ;; set
리스트는 다음과 같이 소괄호()로 둘러싸인다.
(a b c)
리스트는 여타 다른 컬렉션과는 다르게 평가된다. 리스트의 첫번째 요소는 다음과 같이 3가지로 해석되면서 나머지 요소의 해석도 달라진다. 만약 첫번째 요소가
리스트의 첫 요소가 위 세가지가 아닌 다른 문구가 되면 리스트의 평가는 실패하고 예외가 발생한다. 리스트가 첫 요소가 위와 같이 평가되지 않게 하려면 작은 따옴표 '를 괄호앞에 붙이면 된다. 이것이 리스트 리터럴을 만드는 방식이다.
(1 2 3) ;>> ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn... '(1 2 3) ;>> (1 2 3) 빈 리스트 ()는 평가할 요소가 없기 때문에 '를 붙이지 않아도 된다. <code clojure> (= () '()) ;= true
리스프와는 다르게 ()는 nil은 같지 않다.
(= () nil) ;= false
벡터는 다음과 같이 대괄호[]로 둘러싸인다.
[1 2 :a :b]
벡터의 각 요소는 순서대로 평가된다.
맵은 다음과 같이 중괄호{}로 둘러싸인다.
{1 :one, 2 :two, 3 :three}
맵은 키-값 쌍이 한 요소가 된다. 키는 유일해야 한다. 위에서 키는 1, 2, 3 정수이고, 값은 :one, :two, :three 키워드들이다. 맵의 요소들도 벡터와 같이 평가되긴 하지만, 순서는 보장되지 않는다.
집합은 다음과 같이 중괄료{}로 둘러싸은 후 #을 앞에 붙인다.
#{1 2 :one "two"}
집합은 요소들도 평가된다. 요소들은 서로 다른 값이어야 한다. 만약 평가후 같은 값이면 예외가 발생한다.
#{1 (- 2 1)} IllegalArgumentException Duplicate key: 1
클로져에서는 함수는 항상 문구의 첫 요소가 되어야 하는데, 클로져에서는 연산자도 함수이기 때문에 항상 피연산자의 앞에 오게 된다.
(+ 1 2) ;>> 3 (< 1 2) ;>> true
연산자가 피연자 사이에 들어가는 중위 연산자에 익숙한 사람들에게는 전치 연산자가 처음에는 이상하게 보일 수도 있지만, 익숙해지면 괜찮다. 사실 전치 연산자는 더 간결하고 우아한 표현을 가능하게 한다. 다음을 보자.
(+ 1 2 3) ; (1 + 2 + 3) ;>> 6 (< 1 2 3) : (1 < 2) and (2 < 3) ;>> true
중위 연산자의 경우 피연자 만큼이나 반복되어 나타나지만, 전치 연산자를 처음 한 번만 나오면 된다. 또한 비교 연산자의 경우에는 위의 코드에서처럼 간결하면서도 더 의미론적 표현이 가능하다.
익명 함수 리터럴(Anonymous Function Literal)은 임의로 함수를 만들어 내는 유용한 방법이다. #()를 이용한다.
(def sq #(* % %)) ;=> 'user/sq (sq 5) ;>> 25
%는 함수의 파라미터를 나타낸다. #(* % %)는 파라미터 하나를 받아 제곱을 만드는 함수이다. 이 함수를 sq라는 Var에 할당한 후 (sq 5)로 호출하여 사용하였다. 위 코드는 다음과 같이 줄일 수도 있다.
(#(* % %) 5) ;>> 25
익명 함수는 여러 인자를 받을 수 있다.
(#(* %1 %2) 5 4) ;>> 20 (#(* %1 %2 %3 %4) 5 4 3 2) ;>> 120
%1, %2, %3, %4는 익명 함수의 첫 번째, 두 번째, 세 번째, 네 번째 파라미터이다. 파라미터가 하나 밖에 없을 때는 %와 %1로 써도 되지만, 여러 개 있을 때는 이와 같이 구분해주어야 한다. 재미있는 것은 곱셉*에 인자가 여러 개 들어가고 있다는 것이다.
한가지 주의할 점은 익명 함수안에서 다시 익명 함수를 사용할 수는 없다는 것이다.
(#(+ #(* % %) #(* % %)) 3 4) ;=> fail
클로저는 2개의 quoting 문구를 제공한다. quote와 syntax-quote. 클로저 데이타는 곧 코드라고 하였다. 즉 어떤 클로저 문구이든 바로 코드로 평가된다. 때로는 클로저 데이타를 코드로 평가하지 않고 싶을 때가 있는데, 그럴 때 qouting을 사용한다. 즉 quoting은 평가를 막는다.
quote 특수 문구는 자신의 모든 인수를 평가하지 않는다.
(def x 1) ;=> #'user/x x ;=> 1 (quote x) ;=> x (quote (+ x 1)) ;=> (+ x 1) (= (quote (+ x 1)) '(+ x 1)) ;=> true
'는 quote를 대체하는 리더 슈거이다.
syntax-quote은 quote과 같다. 다만 다른 것은 심볼들이 이름 공간이 붙는다는 것이다.
'(+ x 1) ;=> (+ x 1) `(+ x 1) ;=> (clojure.core/+ user/x 1)
quoting되는 문구중에 일부는 평가되게 하고 싶을 때 unquote를 사용한다. ~이 syntax-quote에서 사용되면 unquote된다.
`(+ 1 (* 2 3)) ;=> (clojure.core/+ 1 (clojure.core/* 2 3)) `(+ 1 ~(* 2 3)) ;=> (clojure.core/+ 1 6) (let [x 2] `(1 ~x 3)) ;=> (1 2 3)
(let [x '(2 3)] `(1 ~x)) ;=> (1 (2 3)) (let [x '(2 3)] `(1 ~@x)) ;=> (1 2 3)
'~@'에서 '@'는 시퀀스 x를 unpack하라는 것이다.
Var는 클로저에서 변화를 처리하는 한 가지 방법이다. Var는 다른 언어의 변수와 같은 것은 아니지만 비슷한 역할을 한다. 클로저에서 Var는 스레드별로 동적으로 다시 바운딩되는 변화가능한 저장 장소로서의 역할을 한다. 모든 Var는 대개 루트 바운딩되는데, 이 루트 바운딩은 모든 스레드에서 공유된다. 심볼에 의해 이름 지어진 하나의 Var는 하나의 값을 가리킨다. 이 값은 변할 수 있다.
def 특수 문구(defn, defmacro, defmulti, defonce 등 def에서 파생된 변종 문구들도 포함해서) 하나의 전역 Var 를 만든다.
만약 Var가 기존에 없다면 언바운드된 Var가 리턴된다.
(def x) ;=> #'user/x x #<Unbound Unbound: #'user/x>
(def x 1) ;=> 'user/x x => 1
위 코드는 심볼 x와 정수 1이 바운딩해서 전역 Var인 'user/x 를 리턴한다. x를 참조하면 1이 리턴된다.
심볼의 참조는 결국 다음과 같이 Var라는 단계를 거치는 것이다.
Symbol ==> Var ==> Value
이름 공간은 심볼과 Var 오브젝트들에 대한 전역 맵을 관리한다. 만약 def 문구에서 사용된 심볼이 현재의 이름 공간에서 인턴된 엔트리를 찾지 못하면 새로 만들고, 찾으면 그 심볼에 해당되는 Var를 사용한다. 이러한 과정을 인터닝이라고 한다. 인터닝된 Var는 전역이고, 인터닝되지 않은 Var는 스레드 로컬 Var이다.
Var가 사용되는 것을 보면 마치 변수같다.
(def k 10) (def k 20) (def m 30)
Var는 다음과 같이 전역 변수처럼 사용된다.
(def k 20) (print k) ; print 20 (#(println k)) ; print 20
하지만 다음을 보자
(def k 20) (#(def k 30)) (print k) ; print 30 (#(println k)) ; print 30
함수 안에서 k는 로컬로 형성되는 것이 아니라, k의 전역 바인딩이 바뀌게 되는 것이다. 심지어 함수 안에서 만들어진 Var도 전역이다.
(#(def m 100)) (print m) ; print 100
다음을 보자.
(def m 10) (defn foo [m] (def m 20) (+ m 1)) (foo m) ; returns 11 (+ m 1) ; prints 21
함수 파라미터는 로컬이면서 변경 불가능이다. 따라서 함수안에서의 def 문구는 전역 Var m을 바꾸는 것이지 파라미터 m을 바꾸는 것이 아니다. foo 함수가 호출되는 함수 파라미터로 정수 10이 전달되고 함수안에서 덧셈이 수행되는 것은 이 파라미터 m에 대해서이기 때문에 결과는 11이 리턴된다.
var라는 특수 문구를 사용하면 Var를 직접 참조할 수도 있다. 리더 매크로 #'를 사용해도 된다.
(var k) #'k ;=> #'user/k
REPL에는 편리한 특수 변수가 몇 개 있다.
(+ 1 2) ;=> 3 (+ 3 4) ;=> 7 [*1 *2] ;=> [7 3] (1 2 3) ;>> ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn *e #<ClassCastException java.lang.ClassCastException: java.lang.Long cannot be cast to clojure.lang.IFn>
let은 let문구가 사용되는 영역에서 참조하는 심볼을 만든다. 로컬 변수를 만드는 것과 같다.
(let [x 10 y 20] (+ x y)) ;=> 30
인수 분해(destructuring)때문에 바인딩 벡터는 컴파일 타임에 해석된다.
이름 공간은 심볼을 Var나 자바 클래스에 묶는 동적 매핑이다. 이름 공간은 자바의 패키지, 파이썬이나 루비의 모듈과 비슷한 개념이다. 클로저의 모든 코드와 데이타는 이름 공간안에서 정의된다. 기본 이름 공간은 user이다.
user=> *ns* #<Namespace user> user=> (def a 1) #'user/a user=> a 1 user=> (ns abc) nil abc=> *ns* #<Namespace abc> abc=> a CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context abc=> user/a 1
클로저가 제공하는 *ns*라는 Var는, 항상 현재의 이름 공간을 갖고 있다. REPL의 프롬프트는 현재의 이름 공간을 나타낸다. ns 문구는 abc라는 이름 공간(없으면 새로 만들어서)으로 전환한다. a는 user 이름 공간에서 정의되었기 때문에 abc 이름 공간에서는 찾을 수가 없다. user/a처럼 다른 이름 공간임을 명시해 주어야 한다.
user=> (+ 1 1) 2 user=> (in-ns 'abc) #<Namespace abc> abc=> (+ 1 1) CompilerException java.lang.RuntimeException: Unable to resolve symbol: + in this context abc=> (clojure.core/+ 1 1) 2
ns는 이름 공간을 바꾸는 매크로인데 반해, in-ns는 같은 역할을 하는 함수이다. in-ns의 경우 파라미터에 '를 붙여서 평가되지 않게 해야 한다. 이름 공간이 바뀌면 단지 특수 문구만이 기본으로 정의되어 있을 뿐이다. 다른 이름 공간으로부터 코드를 로드하고 vars를 새로운 이름 공간으로 매핑해야 한다.
abc=> (clojure.core/refer 'user) nil abc=> a 1
fn은 함수를 만드는 특수 문구이다.
(fn [x] (+ 10 x))
함수의 fn 다음에 파라미터를 기술하는 바인딩 벡터가 오고, 그 뒤로 함수의 몸체 문구가 온다. 몸체에는 문구가 여러 개 올 수 있는데 제일 마지막 문구의 평가값이 함수의 리턴값이다. 아래는 여러 개의 파라미터를 갖는 함수 정의이다.
((fn [x y x] (+ x y z)) 1 2 3) ;=> 6
다음은 여러 개의 파라미터 목록(multiple arities)를 갖는 함수 정의이다.
(def my-sub (fn ([x y] (- x y)) ([x y z] (- (+ x y) z)))) ;=> #'user/my-sub (my-sub 10 3) ;=> 7 (my-sub 10 5 3) ;=> 12
클로저에서 함수를 만드는 기본적인 방법은 defn을 사용하는 것이다.
(defn my-add [x y z] (+ x y z)) (defn my-sub ([x y] (- x y)) ([x y z] (- (+ x y) z))))
여러 개의 문구를 한 단위로 평가하고 싶을 때 do 코드 블럭을 사용한다.
(do (println "hi") (+ 1 2)) ;>> hi ;=> 3
함수는 do 코드 블럭이 내부적으로 사용된다. 하지만 익명 함수 리터럴의 경우 명시적으로 do를 사용해야 한다.
(defn f [x y] (println "hi") (+ x y)) #(do (println "hi") (+ %1 %2))
(defn my-add [x & rest] (- x (apply + rest))) ;=> #'user/my-add (my-add 10 1 2 3) ;=> 4 (my-add 10 1 2 3 4) ;=> 0 (#(- % (apply + %&)) 10 1 2 3 4) ;=> 0
if는 클로저에서 제공하는 조건 판단하는 유일한 특수 문구이다.
(if test then else?)
if 특수 문구 다음에 나오는 test 문구를 평가하는데, 만약 test가 nil이나 false가 아니면, then 문구만 평가한다. 만약 test 문구가 nil이나 false일 때, else 문구가 없으면 nil을 반환하고, else 문구가 있으면 else 문구를 평가한 결과를 리턴한다.
클로저의 다른 조건 판단 문구들은 모두 if에 기반해 있기 때문에, nil과 false가 아닌 모든 값들에 대해서는 true로 처리한다.
(if "hi" \t) ;=> \t (if 42 \t) ;=> \t (if nil \t \f) ;=> \f (if false \t) ;=> nil (if (not true) \t) ;=> nil
if외에 아래와 같은 조건 판단 문구가 있다.
recur 특수 문구는 다음과 같이 사용된다.
(recur exprs*)
recur 다음에 문구들이 아예 없거나 여러 개 올 수 있는데, 모두 순서대로 평가된다. 평가 이후 제어는 가장 최근 재귀점으로 돌아가는데, 재귀점은 함수이거나 loop 문구이다. 평가된 결과값들은 재귀점이 함수일 경우 함수 파라미터들과 다시 바인딩되고, 재귀점이 loop 인 경우에는 loop 바인딩에 다시 바인딩된다. 바인딩시 바인딩 순서가 매치되어야 한다.
(loop [x 5] (if (neg? x) x (recur (dec x)))) ;=> -1
위 코드에서 loop은 recur를 위한 재귀점이면서, 초기값을 설정하는 바인딩 벡터를 갖는다. x는 처음 5가 된다. if 문구는 loop을 빠져나오는 조건 판단을 제공하는 것으로, 이것은 반드시 있어야 하는데, 없으면 무한 루프가 되기 때문이다. recur 문구에서 dec 문구에서 x가 하나 줄어든 값으로 평가되어, 이 값은 4가 되는데, 이것은 제어가 재귀점인 loop으로 이동하면서 loop 의 바인딩에 다시 바인딩된다. 그래서 이번에는 x가 4가 된다. 이런 식으로 반복되다가 if 문구에서 x가 -1 일 때 loop를 빠져나오면서 x를 반환하게 된다. 여기서 주의할 점은 로컬 심볼 x는 중간에 변경되지는 않았다는 것이다.
다음은 recur의 재귀점이 함수인 경우이다.
(defn countdown [x] (if (zero? x) :blastoff! (do (println x) (recur (dec x))))) ;= #'user/countdown (countdown 5) ;>> 5 ;>> 4 ;>> 3 ;>> 2 ;>> 1 ;=> :blastoff!
recur는 스택 공간을 전혀 사용하지 않는다. 꼬리 호출(tail-call) 최적화 1)는 되지 않는다. recur 호출은 꼬리 호출이어야 한다. 꼬리 호출이 아닌 recur 호출은 컴파일러에 의해 에러로 잡힌다.
(fn [x] (recur x) (println x)) ; java.lang.UnsupportedOperationException: Can only recur from tail position
클로저는 자바 객체 생성, 정적 메소드 및 개체 메소드 호출, 개체 필드 접근 등을 클로저 코드에서 직접 할 수 있는 강력한 상호운영성을 제공한다. 특수 문구 '.', new, set! 등이 제공된다. 하지만 이것보다는 리더에서 제공하는 슈거가 더 많이 사용된다.
Operation | Java code | Sugared interop form | Equivalent special form usage |
---|---|---|---|
Object instantiation | new java.util.Array.List(100) | (java.util.ArrayList. 100) | (new java.util.ArrayList 100) |
Static method call | Math.pow(2, 10) | (Math/pow 2 10) | (. Math pow 2 10) |
Instance method call | “hello”.substring(1, 3) | (.substring “hello” 1 3) | (. “hello” substring 1 3) |
Static field access | Integer.MAX_VALUE | Integer/MAX_VALUE | (. Integer MAX_VALUE) |
Instance field access | someObject.someField | (.someField someObject) | (. someObject someField) |
클로저는 변경 불가능을 강조하지만 자바 클래스나 객체의 필드를 설정을 위해서 set!을 제공한다. 이에 대해서는 자바 상호운용성 장에서 자세히 알아본다.