이번 장에서는 클로저의 제어 구문에 대해 알아본다.
클로저는 여러가지 제어 구문을 제공하지만, 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" 이 반환되었다. |