이번 장에서는 클로저의 제어 구문에 대해 알아본다.
클로저는 여러가지 제어 구문을 제공하지만, if 특수 형식(special form)을 제외한 나머지
모든 제어 구문은, if 특수 형식을 내부적으로 감싼 매크로로 구현되어 있다. 따라서 가장
중요한 제어 구문은 if이다.
1. 논리적 true와 논리적 false
클로저의 제어 구문에서 참과 거짓을 판별할 때, false와 nil을 제외한 모든 값은
'논리적으로 true’이다. 오로지 false와 nil만이 '논리적으로 false’인 것으로
취급된다. 따라서 0이나 빈 문자열 "", 빈 컬렉션인 (), [], {}, #{} 모두
논리적인 true로 평가된다는 점에 유의하자.
2. if
(if test then-part else-part?)
if 구문은 test 부분을 평가한 결과가 논리적으로 참이면 then-part를, 논리적으로
거짓이면 else-part를 평가한다.
(if (< 5 10) "yes" "no") ;=> "yes"
(if (> 5 10) "yes" "no") ;=> "no"
test 부분을 평가한 결과가 논리적 거짓일 때, else-part가 제공되지 않으면 nil을
반환한다.
(if (> 5 10) "yes") ;=> nil
then-part나 else-part에서 여러 개의 식을 평가하고자 할 때에는 do 특수 형식으로
감싸 주어야 한다.
(if (< 5 10)
(do
(println "5 is less than 10.")
"yes")
(do
(println "This will not be printed.")
"no"))
;>> 5 is less than 10.
;=> "yes"
|
맨 마지막 식을 제외한 그 앞에 있는 모든 식들은 결과적으로 부수 효과(side effect)를 수행하게 된다.
다음의
|
if-not은 if와 정반대의 동작을 수행한다.
(if-not (zero? 0) "not zero" "zero")
;=> "zero"
3. when
(when test expression+)
when 구문은 test 부분이 논리적인 참으로 평가될 때, 그 뒤의 모든 식을 내부적으로
do로 감싸 평가한다. 그래서 다음의 두 식은 완전히 동일하다.
(when (= 1 1)
(+ 1 2)
(+ 3 4)
(+ 5 6))
;=> 11
(if (= 1 1)
(do
(+ 1 2)
(+ 3 4)
(+ 5 6)))
;=> 11
그리고 test 부분이 논리적인 거짓으로 평가될 때에는 단순히 nil을 반환한다.
(when (= 1 2)
(+ 1 2)
(+ 3 4)
(+ 5 6))
;=> nil
그래서 when은 if의 then-part가 여러 개의 식들로 이루어져 있고, else-part는
필요 없을 때 사용된다.
when-not은 when과 정반대의 동작을 수행한다.
(when-not (= 1 2)
(+ 1 2)
(+ 3 4)
(+ 5 6))
;=> 11
(when-not (= 1 1)
(+ 1 2)
(+ 3 4)
(+ 5 6))
;=> nil
4. if-let/when-let
(if-let [local-symbol expression] then-part else-part?)
if-let은 먼저 expression을 평가한 결과를 local-symbol에 바인딩한다. 그후에
바인딩된 값이 논리적 참이면 then-part를, 그렇지 않으면 else-part를 실행한다.
(defn if-let-demo [arg]
(if-let [a arg]
(str "arg: " a)
"no"))
(if-let-demo 10) ;=> "arg: 10"
(if-let-demo nil) ;=> "no"
(if-let-demo false) ;=> "no"
실제로 클로저로 코딩하다 보면 다음과 같은 패턴을 자주 접하게 된다.
(defn drop-one [coll]
(let [s (seq coll)]
(if s
(rest s)
coll)))
(drop-one [1 2 3]) ;=> (2 3)
(drop-one []) ;=> []
이럴 때 다음과 같이 if-let을 사용하면 코드가 간결해진다.
(defn drop-one2 [coll]
(if-let [s (seq coll)]
(rest s)
coll))
(drop-one2 [1 2 3]) ;=> (2 3)
(drop-one2 []) ;=> []
if-let을 사용할 때 주의할 점은 다음과 같다.
-
let과는 달리,if-let은 지역 심볼을 한 개만 바인딩할 수 있다. 두 개 이상 바인딩하면 다음과 같이 예외가 발생한다.(if-let [a 10 b 20] (+ a b) "no") :>> IllegalArgumentException if-let requires exactly 2 forms in binding vector이 문제를 우회하려면
if-let을 중첩해 사용해야 한다.(if-let [a 10] (if-let [b 20] (+ a b) "no")) ;=> 30 -
바인딩된 지역 심볼을
else-part에서 참조해서는 안된다. 사용하면 다음과 같은 예외가 발생한다.(if-let [a 10] (+ a 20) (str "arg: " a)) ;>> CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context
when-let과 if-let의 관계는 when과 if의 관계와 같다.
(defn drop-one3 [coll]
(when-let [s (seq coll)]
(println s)
(rest s)))
(drop-one3 [1 2 3])
;>> (1 2 3)
;=> (2 3)
(drop-one3 [])
;=> nil
5. cond
cond 구문은 C 언어의 switch 구문과 유사하다.
(cond clause+) clause := test expression
이 구문은 나열된 clause를 차례대로 실행하다가, 최초로 논리적 참을 반환하는 test를
만나면 그에 해당하는 expression을 평가한 후, 그 결과를 cond 구문의 결과로 즉시
반환한다.
(defn pos-neg-or-zero [n]
(cond
(< n 0) "negative"
(> n 0) "positive"
:else "zero"))
(pos-neg-or-zero 5) ;=> "positive"
(pos-neg-or-zero -1) ;=> "negative"
(pos-neg-or-zero 0) ;=> "zero"
맨 마지막 clause의 test 부분은, 위의 코드에서처럼 관례적으로 :else라는
키워드를 사용하지만, 논리적인 참값을 반환하는 어떤 식(expression)도 가능하다. 예를 들어,
현실성은 없지만 다음과 같이 할 수도 있다. 즉, (= 5 5)는 언제나 true를 반환하므로
default인 경우의 처리가 가능하다.
(defn pos-neg-or-zero2 [n]
(cond
(< n 0) "negative"
(> n 0) "positive"
(= 5 5) "zero"))
(pos-neg-or-zero2 0) ;=> "zero"
만약 해당하는 clause가 한 개도 없을 때에는 nil을 반환한다.
(defn pos-neg-or-zero3 [n]
(cond
(< n 0) "negative"
(> n 0) "positive"))
(pos-neg-or-zero3 0) ;=> nil
6. condp
(condp pred expr clause+ default-expr?)
clause := test-expr result-expr | test-expr :>> result-fn
pred := 두 개의 인수를 받는 함수이어야 한다.
이 함수의 첫 번째 인자로 clause 부분의 test-expr이 들어가고, 두 번째 인자로 expr이 들어간다.
결과적으로 (pred test-expr expr) 식이 실행된다.
result-fn := 한 개의 인수를 받는 함수이어야 한다.
(pred test-expr expr) 식을 평가한 결과가 이 함수의 인수로 들어간다.
default-expr := 디폴트 처리를 담당한다.
다음과 같은 cond 구문이 있다고 할 때,
(defn cond-demo [value]
(cond
(instance? Number value) (* value 2)
(instance? String value) (* (count value) 2)
:else "Unexpected type."))
condp 구문을 이용하면, 다음과 같이 더 간단하게 표현할 수 있다[1].
(defn condp-demo [value]
(condp instance? value
Number (* value 2)
String (* (count value) 2)
"Unexpected type.")) ; (1)
(condp-demo 10) ;=> 20
(condp-demo "Clojure") ;=> 14
(condp-demo :hello) ;=> "Unexpected type."
| 1 | default-expr 부분은 식이 한 개뿐인 것에 주목하자. |
condp 구문은 cond 구문과는 달리, 일치하는 절(clause)이 없을 경우에 디폴트 처리부인
default-expr 부분이 없으면 예외가 발생한다.
(defn condp-demo2 [value]
(condp instance? value
Number (* value 2)
String (* (count value) 2)))
(condp-demo2 :hello)
:>> IllegalArgumentException No matching clause: :hello
clause 부분이 다음과 같이 세 개의 식으로 이루어질 수도 있다.
test-expr :>> result-fn
이때 두 번째 식은 반드시 :>> 키워드이어야 하고, 세 번째 식은 인수가 하나인 함수이어야
한다. (pred test-expr expr) 식을 평가한 결과가 이 함수의 인수로 들어간다.
(defn condp-demo3 [value]
(condp some value
#{1 2 3} :>> inc
#{4 5 6} :>> dec
#{7 8 9} :>> #(+ % 3)))
;; (some #{1 2 3} [1 2 3]) => 1
;; (some #{4 5 6} [6 5 4]) => 6
;; (some #{7 8 9} [8 7 9]) => 8
(condp-demo3 [1 2 3]) ;=> 2
(condp-demo3 [6 5 4]) ;=> 5
(condp-demo3 [8 7 9]) ;=> 11
7. case
(case expr clause+ default-expr?)
clause := test-constant result-expr |
(test-constant1 ... test-constantN) result-expr
case 구문[2]은 clause 내의 test-constant 부분에 나열된
값들이 컴파일 타임에 그 값을 알 수 있어야 한다는 제약이 있다. 따라서 문자열이나 키워드
같은 자기 자신으로 평가되는 값들이 주로 나열된다.
(defn case-demo [value]
(case value
"" 0
"hello" (count value)))
(case-demo "hello") ;=> 5
condp와 마찬가지로, 일치하는 절(clause)이 없을 경우에 디폴트 처리부인 default-expr
부분이 없으면 예외가 발생한다.
(case-demo "hi")
;>> IllegalArgumentException No matching clause: hi
다음은 default-expr 부분을 제공한 예이다.
(defn case-demo2 [value]
(case value
"" 0
"hello" (count value)
"no match")) ; (1)
(case-demo2 "hi") ;=> "no match"
| 1 | default-expr 부분은 식이 한 개뿐인 것에 주목하자. |
clause의 test-constant 부분에 다음과 같이 심볼이 올 수도 있다. 이 심볼은 런타임에
평가되지 않고, 컴파일시에 (앞에 인용 기호가 자동으로 붙는) 심볼로 컴파일된다.
(def x 10)
(let [value 'x]
(case value
x "x"
y "y"
z "z"
"no-match")
;=> "x"
위의 코드에서 case 내부의 x는 런타임에 10으로 평가되지 않고, 컴파일시에 심볼
'x로 값이 바뀌에 된다. 따라서 런타임에 심볼 'x와 일치하게 되어, 결과적으로
"x"라는 문자열을 반환하게 된다.
위의 코드는 다음처럼 심볼 x, y, z를 리스트로 묶어 한꺼번에 처리할 수도 있다. 이
경우에는 value가 심볼 x나 y 또는 z이면 "x, y or z"를 반환하게 된다.
(let [value 'x]
(case value
(x y z) "x, y or z" ; (1)
"no-match")
| 1 | 여기서 (x y z)는 x라는 함수를 인수 y와 z에 적용하라는 의미가 아닌 것에
주의해아 한다. (x y z)는 컴파일시에 '(x y z)의 형태로 리스트 자료형 자체로
컴파일되기 때문이다. |
몇 가지 예를 더 들어 본다.
(let [value ()]
(case value
(()) "empty seq" ; (1)
((1 2)) "my seq"
"no match"))
;=> "empty seq"
(let [value ()]
(case value
[] "empty seq" ; (2)
((1 2)) "my seq"
"no match"))
;=> "empty seq"
(let [value [1 2]]
(case value
[] "empty vec"
(vec (1 2)) "my vec" ; (3)
"no match"))
;;=> "my vec"
| 1 | (())에서 바깥의 괄호는 test-expr의 시작과 끝을 알리는 기호로 쓰이므로, 빈 리스트를
나열하고자 할 때는 그 안에 다시 나열해 주어야 한다. |
| 2 | 그래서 빈 리스트를 검사하고자 할 때는, (())처럼 해 주기 보다는 []로 해 주는 것이
낫다. 왜냐하면 (= () [])의 평가 결과는 true이기 때문이다. |
| 3 | (= [1 2] '(1 2))의 결과는 true이므로, "my vec"이 반환되었다. |