특수 형식

특수 형식 (Special Forms)

특수 형식(special forms)은 표준 Clojure 평가 규칙과 다른 평가 규칙을 가지며, Clojure 컴파일러가 직접 이해해요. 아래 제목은 정규 표현식 문법으로 특수 형식 문법을 비공식적으로 서술해요: ?(선택), *(0개 이상), +(1개 이상). 비-비터미널(비말단)은 _기울임꼴_로 표시돼요.

출처: Special Forms

본문

(def symbol doc-string? init?)

이름이 _symbol_이고 현재 네임스페이스(*ns*)의 값인 네임스페이스를 가진 전역 var을 생성하고 인터닝(intern)하거나 찾아요. _init_이 제공되면 평가되어 var의 루트 바인딩이 그 결과 값으로 설정돼요. _init_이 제공되지 않으면 var의 루트 바인딩은 영향받지 않아요. def는 var가 호출 시점에 스레드-바인딩돼 있어도 항상 루트 바인딩에 적용돼요. def는 var 자체(값이 아님)를 산출해요. _symbol_이 이미 네임스페이스에 있고 인터닝된 var에 매핑되지 않았다면 예외를 던져요. doc-string 지원은 Clojure 1.3에서 추가됐어요.

_symbol_에 있는 모든 메타데이터는 평가되어 var 자체의 메타데이터가 돼요. 특별한 해석을 가진 메타데이터 키가 몇 가지 있어요:

  • :private — var의 접근 제어를 나타내는 boolean. 이 키가 없으면 기본 접근은 public이에요(마치 :private false인 것처럼).
  • :doc — var 내용에 대한 짧은(1-3줄) 문서를 담은 문자열.
  • :test — 여러 연산을 assert로 검사하는 인자 없는 fn. 메타데이터 맵 안의 리터럴 fn을 평가하는 동안 var 자체에 접근할 수 있어요.
  • :tag — var 안 객체의 Java 타입이나, 객체가 fn이면 그 반환 값을 나타내는 클래스 이름의 symbol 또는 Class 객체.

그 외에도 컴파일러가 var에 다음 메타데이터 키를 넣어요:

  • :file 문자열
  • :line int
  • :name 단순 symbol
  • :ns var가 인터닝된 네임스페이스
  • :macro var가 매크로를 이름 짓는 경우 true
  • :arglists defn에 제공된 인자 형식들의 벡터(들)의 리스트

var 메타데이터는 애플리케이션 특정 목적으로도 사용할 수 있어요. 충돌을 피하려면 네임스페이스-한정 키(예: :myns/foo)를 사용하는 것을 고려해요.

(defn
 ^{:doc "mymax [xs+] gets the maximum value in xs using > "
   :test (fn []
             (assert (= 42  (mymax 2 42 5 4))))
   :user/comment "this is the best fn ever!"}
  mymax
  ([x] x)
  ([x y] (if (> x y) x y))
  ([x y & more]
   (reduce mymax (mymax x y) more)))

user=> (meta #'mymax)
  {:name mymax,
   :user/comment "this is the best fn ever!",
   :doc "mymax [xs+] gets the maximum value in xs using > ",
   :arglists ([x] [x y] [x y & more])
   :file "repl-1",
   :line 126,
   :ns #<Namespace user >,
   :test #<user$fn__289 user$fn__289@20f443 >}

많은 매크로(예: defn, defmacro)가 def로 확장되므로, 이름으로 쓰인 _symbol_에서 결과 var로 메타데이터도 전달해요.

최상위 레벨 이외에서 def로 var의 루트 값을 변경하는 것은 보통 var를 가변 전역으로 사용한다는 뜻이며, 나쁜 스타일로 간주돼요. var에 스레드-로컬 값을 제공하는 binding을 사용하거나, refagent를 var에 넣고 트랜잭션/액션으로 변경하는 것을 고려해요.

(if test then else?)

_test_를 평가해요. 단일 값 nil 또는 false가 아니면 _then_을 평가해 산출하고, 그렇지 않으면 _else_를 평가해 산출해요. _else_가 제공되지 않으면 기본값 nil이에요. Clojure의 다른 모든 조건문은 같은 논리에 기반해요. 즉 nilfalse는 논리적 거짓을, 그 외 모든 것은 논리적 참을 구성하며, 그 의미는 전체에 걸쳐 적용돼요. if는 boolean Java 메서드 반환 값에 대해 Boolean으로 변환 없이 조건 검사를 수행해요. ifjava.lang.Boolean의 임의 값이 아니라 단일 값 false(Java의 Boolean.FALSE)만 검사한다는 점에 유의해요. 따라서 직접 박싱된 Boolean을 만들 때는 Boolean 생성자가 아니라 Boolean/valueOf를 사용해야 해요.

(do expr*)

__expr__들을 순서대로 평가하고 마지막 값의 값을 반환해요. 표현식이 없으면 nil을 반환해요.

(let [ binding* ] expr*)

binding => binding-form init-expr

__binding-form__들의 symbol들이 각자 init-expr 또는 그 일부에 바인딩된 어휘 문맥에서 __expr__들을 평가해요. 바인딩은 순차적이라 각 _binding_은 이전 바인딩을 볼 수 있어요. __expr__들은 암시적 do 안에 포함돼요. binding symbol에 메타데이터 태그가 주석으로 달리면, 컴파일러는 태그를 클래스 이름으로 풀어 그 타입을 이후의 binding 참조에 가정하려고 시도해요. 가장 단순한 _binding-form_은 symbol로, 전체 _init-expr_에 바인딩돼요:

(let [x 1
      y x]
  y)
-> 1

binding symbol의 :tag 메타데이터가 FunctionalInterface로 주석된 Java 인터페이스라면, __init-expr__는 (필요하면) 지정된 인터페이스로 coerce돼요:

(let [coll (java.util.ArrayList. (range 10))
      ^java.util.function.Predicate pred even?]
  (.removeIf coll pred) ;; coll을 변경
  coll)
-> [1 3 5 7 9]

바인딩 형식에 대한 자세한 내용은 바인딩 형식을 참조해요.

let으로 만든 지역은 변수가 아니에요. 한 번 생성되면 그 값은 절대 변하지 않아요!

(quote form)

평가되지 않은 _form_을 산출해요.

user=> '(a b c)
(a b c)

함수 a를 호출하려는 시도가 없음에 유의해요. 반환 값은 symbol 3개의 리스트예요.

(var symbol)

_symbol_은 var로 풀려야 하고, Var 객체 자체(값이 아님)가 반환돼요. 리더 매크로 #'x(var x)로 확장돼요.

(fn name? [params* ] expr*)

(fn name? ([params* ] expr*)+)

params => positional-param* , 또는 positional-param* & rest-param + positional-param => binding-form + rest-param => binding-form + name => symbol

함수(fn)를 정의해요. fn은 IFn 인터페이스를 구현하는 일급 객체예요. IFn 인터페이스는 0-20의 arity 범위로 오버로드된 invoke() 함수를 정의해요. 단일 fn 객체는 하나 이상의 invoke 메서드를 구현할 수 있어서 arity로 오버로드될 수 있어요. 하나의 오버로드만이 가변적(variadic)일 수 있으며, 앰퍼샌드 뒤에 단일 _rest-param_을 지정해요. 이런 가변 진입점은 위치 파라미터를 초과하는 인자로 호출되면 그것들을 seq로 모아서 rest param에 바인딩하거나 비구조화해요. 제공된 인자가 위치 파라미터를 초과하지 않으면 rest param은 nil이 돼요.

첫 번째 형식은 단일 invoke 메서드를 가진 fn을 정의하고, 두 번째 형식은 하나 이상의 오버로드된 invoke 메서드를 가진 fn을 정의해요. 오버로드들의 arity는 구별돼야 해요. 두 경우 모두 표현식의 결과는 단일 fn 객체예요.

__expr__들은 _params_가 실제 인자에 바인딩된 환경에서 컴파일돼요. __expr__들은 암시적 do 안에 포함돼요. 이름 __symbol__이 제공되면 함수 정의 안에서 함수 객체 자체에 바인딩되어, 익명 함수에서도 자기 호출이 가능해요. param symbol에 메타데이터 태그가 주석으로 달리면 컴파일러는 태그를 클래스 이름으로 풀어 이후 바인딩 참조에 그 타입을 가정하려고 시도해요.

(def mult
  (fn this
      ([] 1)
      ([x] x)
      ([x y] (* x y))
      ([x y & more]
          (apply this (this x y) more))))

mult 같은 이름 있는 fn은 보통 위와 같은 것으로 확장되는 defn으로 정의돼요.

fn(오버로드)은 함수의 맨 위에 재귀 지점을 정의하며, 그 arity는 rest param을 포함한다면 포함해서 param 수와 같아요. recur를 참조해요.

fn은 Java의 Callable, Runnable, Comparator 인터페이스를 구현해요.

1.1 이후

함수는 런타임 전/후 조건(pre/post-conditions) 지정을 지원해요.

함수 정의의 문법은 다음과 같이 됩니다:

(fn name? [param* ] condition-map? expr*)

(fn name? ([param* ] condition-map? expr*)+)

문법 확장은 fn 형식으로 확장되는 defn과 다른 매크로에도 적용돼요.

참고: 파라미터 벡터 뒤의 유일한 형식이 맵이면 그것은 함수 본문으로 취급되며, 조건 맵이 아니에요.

condition-map 파라미터는 함수의 전/후 조건을 지정하는 데 사용돼요. 다음과 같은 형태예요:

{:pre [pre-expr*]
 :post [post-expr*]}

여기서 어느 키든 선택적이에요. 조건 맵은 arglist의 메타데이터로도 제공될 수 있어요.

_pre-expr_와 _post-expr_는 함수의 파라미터를 참조할 수 있는 boolean 표현식이에요. 또한 %는 _post-expr_에서 함수의 반환 값을 참조하는 데 사용할 수 있어요. 조건 중 하나가 false로 평가되고 *assert*가 참이면 java.lang.AssertionError 예외가 던져져요.

예:

(defn constrained-sqr [x]
    {:pre  [(pos? x)]
     :post [(> % 16), (< % 225)]}
    (* x x))

바인딩 형식에 대한 자세한 내용은 바인딩 형식을 참조해요.

(loop [binding* ] expr*)

loop는 바인딩 수와 같은 arity로 루프 맨 위에 재귀 지점을 설정한다는 점을 제외하면 let과 정확히 같아요. recur를 참조해요.

(recur expr*)

__expr__들을 순서대로 평가한 다음, 병렬로 재귀 지점의 바인딩들을 __expr__들의 값으로 다시 바인딩해요. 재귀 지점이 fn 메서드였다면 파라미터를 다시 바인딩하고, 재귀 지점이 loop였다면 loop 바인딩을 다시 바인딩해요. 그다음 실행은 재귀 지점으로 다시 점프해요. recur 표현식은 재귀 지점의 arity와 정확히 일치해야 해요. 특히 재귀 지점이 가변 fn 메서드의 맨 위였다면 rest 인자 수집은 없어요 — 단일 seq(또는 null)를 전달해야 해요. 꼬리 위치(tail position)가 아닌 곳에서의 recur는 오류예요.

recur는 Clojure에서 스택을 소모하지 않는 유일한 루핑 구성이라는 점에 유의해요. 꼬리 호출 최적화(tail-call optimization)는 없으며, 알 수 없는 경계의 루핑에 자기 호출을 사용하는 것은 권장되지 않아요. recur는 함수형이며 꼬리 위치에서의 사용은 컴파일러가 검증해요.

(def factorial
  (fn [n]
    (loop [cnt n acc 1]
       (if (zero? cnt)
            acc
          (recur (dec cnt) (* acc cnt))))))

(throw expr)

_expr_가 평가되고 던져지므로, Throwable의 어떤 파생체 인스턴스를 산출해야 해요.

(try expr* catch-clause* finally-clause?)

catch-clause -> (catch classname name expr) + finally-clause -> (finally expr)

__expr__들이 평가되고, 예외가 발생하지 않으면 마지막 표현식의 값이 반환돼요. 예외가 발생하고 __catch-clause__가 제공되면 각각을 차례로 검사해, 던져진 예외가 그 _classname_의 인스턴스인 첫 번째 것을 매칭되는 __catch-clause__로 간주해요. 매칭되는 _catch-clause_가 있으면 그 __expr__들은 _name_이 던져진 예외에 바인딩된 문맥에서 평가되고, 마지막 값이 함수의 반환 값이 돼요. 매칭되는 __catch-clause__가 없으면 예외는 함수 밖으로 전파돼요. 정상적이든 비정상적이든 반환하기 전에 모든 finally-clause __expr__들은 그 부수 효과를 위해 평가돼요.

(monitor-enter expr)

(monitor-exit expr)

이것들은 사용자 코드에서 피해야 하는 동기화 프리미티브예요. locking 매크로를 사용해요.

그 밖의 특수 형식 (Other Special Forms)

필드의 특수 형식 dot ('.'), new, set!은 레퍼런스의 Java 연동 섹션에 서술돼 있어요.

var의 set!은 레퍼런스의 Vars 섹션에 서술돼 있어요.

바인딩 형식 (Binding Forms) — 비구조화 (Destructuring)

Clojure의 가장 단순한 __binding-form__은 symbol이에요. 그러나 Clojure는 let 바인딩 목록, fn 파라미터 목록, 그리고 확장해서 let이나 fn으로 확장되는 모든 매크로에서 비구조화(destructuring)라 불리는 추상 구조적 바인딩도 지원해요. 비구조화는 유사한 컬렉션을 바인딩 형식으로 사용해 컬렉션 안의 값들에 대한 바인딩 집합을 만드는 방법이에요. 벡터 형식은 순차 컬렉션에서 위치로, 맵 형식은 연관 컬렉션에서 키로 바인딩을 지정해요. 비구조화 형식은 __binding-form__이 나올 수 있는 어디든 나타날 수 있어서 중첩되며, 컬렉션 접근자를 쓰는 것보다 더 명확한 코드를 산출해요.

데이터 부재(즉 순차 구조에 요소가 너무 적고, 연관 구조에 키가 없음 등) 때문에 각자 부분과 맞지 않는 __binding-form__들은 nil에 바인딩돼요.

순차 비구조화 (Sequential destructuring)

벡터 __binding_form__들은 벡터, 리스트, seq, 문자열, 배열, 그리고 nth를 지원하는 무엇이든 같은 컬렉션에서 값을 순차적으로 바인딩해요. 순차 비구조화 형식은 __binding-form__들의 벡터이며, nth로 조회되는 __init-expr__의 연속 요소들에 바인딩돼요. 또한 선택적으로 & 뒤의 __binding-form__은 아직 바인딩되지 않은 나머지 시퀀스 부분에 바인딩되며, nthnext로 조회돼요.

마지막으로, 선택적으로 :as 뒤의 symbol이 전체 __init-expr__에 바인딩돼요:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
  [a b c d e])

->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

이 형식들은 중첩될 수 있어요:

(let [[[x1 y1][x2 y2]] [[1 2] [3 4]]]
  [x1 y1 x2 y2])

->[1 2 3 4]

모든 순차 경우에서 비구조화 바인딩의 __binding-form__들은 목표 데이터 구조 안에서 원하는 값이 있는 위치와 일치해요.

연관 비구조화 (Associative destructuring)

맵 __binding-form__들은 맵, 셋, 벡터, 문자열, 배열 같은 컬렉션(후자 셋은 정수 키를 가짐)에서 값을 조회해 바인딩을 만들어요. 그것은 binding-form->key 쌍의 맵으로 구성되며, 각 __binding-form__은 _init-expr_에서 제공된 키의 값에 바인딩돼요. 또한 선택적으로 바인딩 형식의 :as 키 뒤의 symbol이 전체 __init-expr__에 바인딩돼요. 입력 맵에 없는 키에 기본 값을 제공하려면 :or 지시자를 사용할 수 있는데, 그 값은 바인딩 이름 또는 [1.13 이후] 키를 기본 값에 매핑한 맵이에요.

(let [{a :a, b :b, c :c, :as m :or {a 2 b 3}}  {:a 5 :c 6}]
  [a b c m])

->[5 3 6 {:c 6, :a 5}]

나중에 코드에서 :or 맵에 적은 기본 값들을 다시 적지 않고 캡처하려면 :defaults 지시자 [1.13 이후]를 사용할 수 있는데, 비구조화 동안 만들어진 키→기본 값 맵에 이름을 바인딩해요.

(let [{a :a, b :b, c :c, :as m :or {a 2 b 3 :c 42} :defaults df} {:a 5}] [a b df])

->[5 3 {:a 2, :b 3, :c 42}]

대응하는 맵 키와 같은 이름의 symbol을 바인딩하고 싶은 경우가 많아요. :keys 지시자는 binding-form->key 쌍에서 자주 발견되는 중복을 다뤄줘요:

(let [{fred :fred ethel :ethel lucy :lucy} m] ...

다음과 같이 쓸 수 있어요:

(let [{:keys [fred ethel lucy]} m] ...

맵 비구조화 형식에서 접두사 있는 맵 키를 사용할 수도 있어요:

(let [m {:x/a 1, :y/b 2}
      {:keys [x/a y/b]} m]
  (+ a b))

-> 3

접두사 키 경우에 바인딩된 symbol 이름은 접두사 키의 오른쪽과 같아요. :keys 지시자에서 자동 해석 keyword 형식도 사용할 수 있어요:

(let [m {::x 42}
      {:keys [::x]} m]
  x)

-> 42

문자열과 symbol 키를 매칭하는 유사한 :strs:syms 지시자가 있는데, 후자는 접두사 있는 symbol 키도 허용해요.

Clojure 1.9는 같은 네임스페이스를 공유하는 여러 키(또는 symbol)를 직접 비구조화하는 지원을 다음 비구조화 키 형식으로 추가해요:

  • :__ns__/keys — _ns_가 입력에서 조회할 키의 기본 네임스페이스를 지정해요. keys 요소는 네임스페이스를 지정하면 안 되고, :keys처럼 새 지역 symbol도 정의해요.
  • :__ns__/syms — _ns_가 입력에서 조회할 symbol의 기본 네임스페이스를 지정해요. syms 요소는 네임스페이스를 지정하면 안 되고, :syms처럼 새 지역 symbol도 정의해요.
(let [m #:domain{:a 1, :b 2}
      {:domain/keys [a b]} m]
  [a b])

-> [1 2]

맵 비구조화 동안 필수 키가 존재하는지 확인하고 싶으면 :keys/:syms/:strs 바인딩 지시자의 검사형 변형인 :keys!/:syms!/:strs! [1.13 이후]를 사용할 수 있는데, 입력 맵에 지정된 키 중 하나라도 없으면 예외를 던져요. 이 검사 지시자들은 &도 받으며 [1.13 이후], 그 뒤에 여전히 필수·검사되지만 지역 이름에 바인딩되지는 않는 추가 키를 나열할 수 있어요.

(let [{:keys! [fred ethel lucy & :ricky]} m] ... ;; m은 :fred, :ethel, :lucy, :ricky 키를 가져야 함

바인딩하지 않거나 :select에 포함할 키를 문서화하려면 :keys, :strs, :syms에서 & 뒤에 나열할 수 있어요 [1.13 이후]. & 뒤의 리터럴 키는 :keys [a & :b "c" 'd]처럼 이질적일 수 있지만, symbol은 바인딩 이름과 구분되도록 인용돼야 해요.

다루는 키를 알거나 추가 키를 견디지 못하는 무언가에 데이터를 전달하면서도 프로세스가 받아들이는 것에 유연함을 유지해야 할 때 :select [1.13 이후]를 사용할 수 있어요. :select 지시자는 비구조화 형식에 언급된 키로 제한된, 입력 맵의 부분집합(기본 값으로 보강된)인 맵을 지명해요.

(let [{:keys [a b & :c] :as m :or {a 42 b 3 :c 4} :select sm :defaults df}  {:a 5}]
  [a b m sm df])

-> [5 3 {:a 1} {:c 4, :b 3, :a 5} {:a 42, :b 3, :c 4}]

누락된 키에 기본 값을 대입한 입력 맵 같은 맵을 얻으려면 :all 지시자를 사용해요. [1.13 이후] :all은 중첩된 맵을 통해 깊이 순회해요.

(let [{:keys [a & :b] :as m :or {:a 42 :b 3} :all all-m} {}]
  [a all-m])

-> [42 {:a 42, :b 3}]

키워드 인자 (Keyword Arguments)

키워드 인자는 akey aval bkey bval... 형태의 선택적 꼬리 가변 인자이며, 연관 비구조화를 통해 함수 본문에서 접근할 수 있어요. 또한 Clojure 1.11에서 소개된, kwargs를 받도록 지정된 함수는 키/값 쌍 대신에, 또는 그에 더해(그리고 뒤에) 단일 맵을 전달받을 수 있어요. 단일 맵만 전달되면 그것이 비구조화에 바로 사용되고, 그렇지 않으면 앞선 키/값들로 conj로 만든 맵에 꼬리 맵이 추가돼요. 키워드 인자를 받는 함수를 정의하려면 rest-param 선언 위치에 맵 비구조화 형식을 제공해요. 예를 들어 시퀀스와 선택적 키워드 인자를 받아 값들을 담은 벡터를 반환하는 함수는 다음과 같이 정의돼요:

(defn destr [& {:keys [a b] :as opts}]
  [a b opts])

(destr :a 1)
->[1 nil {:a 1}]

(destr {:a 1 :b 2})
->[1 2 {:a 1 :b 2}]

destr에서 & 오른쪽의 맵 __binding-form__은 위에서 상세히 설명한 연관 비구조화 __binding-form__이에요.

아래 두 foo 선언은 동등하며, 연관 비구조화의 seq 해석을 보여줘요:

(defn foo [& {:keys [quux]}] ...)

(defn foo [& opts]
  (let [{:keys [quux]} opts] ...))

중첩 비구조화 (Nested destructuring)

바인딩 형식은 서로 안에 임의로 중첩될 수 있으므로, 거의 무엇이든 분해할 수 있어요:

(let [m {:j 15 :k 16 :ivec [22 23 24 25]}
      {j :j, k :k, i :i, [r s & t :as v] :ivec, :or {i 12 j 13}} m]
  [i j k r s t v])

-> [12 15 16 22 23 (24 25) [22 23 24 25]]

더 알아보기