Evaluation

Evaluation (평가)

Clojure에서 "평가(evaluation)"는 어떤 형식(form)을 컴파일러가 값으로 바꾸는 과정을 말해요. 코드를 실행한다는 게 결국 이 평가를 거치는 일인데, 정작 그 안에서 어떤 일이 벌어지는지 잘 모르고 쓰는 경우가 많죠. 그 평가의 규칙을 하나씩 짚어볼게요. REPL에서 직접 표현식을 입력해 본 경험이 있다면, 그게 바로 평가의 대표적인 예시예요.

출처: Clojure 공식문서

본문

평가가 일어나는 여러 상황

평가는 다양한 상황에서 일어나요.

  • REPL에서 상호작용적으로
  • 스트림에서 형식들을 읽어 들이는 load / load-file / load-reader / load-string 류의 함수를 통해서
  • eval 을 통해 프로그램적으로

Clojure 프로그램은 식(expression)으로 이루어져 있어요. 특수 형식(special form)이나 매크로가 특별히 다루지 않는 모든 형식은 컴파일러가 하나의 식으로 간주하고, 평가해서 값을 만들어냅니다. Clojure에는 선언(declaration)이나 문장(statement)이라는 개념이 없어요. 다만 부수 효과를 위해 평가하고 그 값은 무시하는 식이 있을 수는 있죠.

어느 상황에서든 평가 방식은 같아요. 하나의 객체를 컴파일러가 보고, 평가한 뒤 결과를 돌려줍니다. 식이 컴파일이 필요하면 컴파일돼요. 별도의 컴파일 단계를 거치지도 않고, 정의한 함수가 "인터프리터로 실행되고 있나" 걱정할 필요도 없어요. Clojure에는 인터프리터가 없습니다.

스스로 평가되는 값들

문자열, 숫자, 문자, true, false, nil, 그리고 키워드(keyword)는 자기 자신으로 평가돼요.

심볼의 해석

심볼(Symbol)은 해석(resolved) 됩니다. 심볼이 어떻게 생겼느냐에 따라 해석되는 대상이 달라져요.

  • 네임스페이스 한정(namespace-qualified) 심볼이면, 그 심볼이 가리키는 전역 var의 바인딩 값이 값이 돼요. 그런 전역 var가 없거나, 다른 네임스페이스의 비공개 var를 참조하면 오류예요.
  • 패키지 한정(package-qualified) 심볼이면, 심볼이 가리키는 Java 클래스가 값이 돼요. 그런 클래스가 없으면 오류이고요.
  • 한정자가 클래스를 가리키는데 이름이 어떤 것인지에 따라 달라져요.
    • 클래스의 정적 필드라면, 그 정적 필드의 값이 값이 돼요.
    • 클래스의 정적 메서드라면, 그 정적 메서드를 호출하는 Clojure 함수가 값이 됩니다. (1.12부터)
    • . 가 인스턴스 메서드를 앞에 붙인 형태라면, 그 인스턴스 메서드를 호출하는 Clojure 함수가 값이 돼요. (1.12부터)
    • new 라면, 생성자를 호출하는 Clojure 함수가 값이 됩니다. (1.12부터)
  • 한정자가 클래스나 프리미티브 타입인데 심볼 이름이 1~9 라면, 한정자를 구성 요소 타입으로 하고 숫자가 차원(dimension)을 나타내는 배열 클래스가 값이 돼요. (1.12부터)
  • 그 외에 한정되지 않은 심볼이라면, 심볼 이름에 대해 다음이 순서대로 적용돼요.
    1. 특수 형식의 이름이면 특수 형식으로 보고 그에 맞게 다룬다.
    2. 로컬 스코프 안(예: 함수 정의나 let 안)이면, 로컬 바인딩(함수 인자나 let에 묶인 이름 같은 것)을 가리키는지 찾아봐요. 맞으면 그 로컬 바인딩의 값이 값이 돼요.
    3. 현재 네임스페이스에서 심볼이 클래스로 매핑돼 있는지 찾아봐요. 있으면 그 심볼은 Java 클래스 객체를 가리키는 것으로 봅니다. 다만 클래스 이름은 평소에는 클래스 객체를 뜻하지만, .new 같은 특정 특수 형식 안에서는 특별히 다뤄진다는 점을 기억해 두세요.
    4. 현재 네임스페이스에서 심볼이 var로 매핑돼 있는지 찾아봐요. 있으면 그 심볼이 참조하는 var의 바인딩 값이 값이 됩니다.
    5. 위 모두 해당하지 않으면 오류예요.

심볼에 메타데이터가 있으면 컴파일러가 그것을 사용할 수는 있지만, 결과 값의 일부가 되지는 않아요.

벡터·셋·맵의 평가

벡터, 셋, 맵은 각각 벡터·(해시)셋·맵을 만들어내는데, 그 내용물은 안에 담긴 객체들을 평가한 값이 됩니다. 벡터의 요소는 왼쪽에서 오른쪽으로 평가되고, 셋과 맵은 순서가 정의되어 있지 않은 채 평가돼요. 메타데이터 맵도 마찬가지예요. 벡터나 맵에 메타데이터가 있으면, 그 평가된 메타데이터 맵이 결과 값의 메타데이터가 됩니다.

user=> (def x 1)
user=> (def y 2)
user=> ^{:x x} [x y 3]
^{:x 1} [1 2 3]

여기서 [x y 3]의 요소들이 각각 평가되어 1, 2, 3이 되고, 메타데이터 맵 {:x x}도 평가되어 {:x 1}이 됐죠. 그 평가된 메타데이터가 결과 벡터의 메타데이터로 붙어요.

리스트의 평가

빈 리스트 ()는 빈 리스트로 평가돼요.

비어 있지 않은 리스트는 특수 형식, 매크로, 또는 함수에 대한 호출(call) 로 간주됩니다. 호출은 (연산자 연산자들*) 형태를 가져요.

  • 특수 형식은 Clojure에 내장된 프리미티브로, 핵심 연산을 수행해요. 호출의 연산자가 특수 형식의 이름으로 해석되는 심볼이면 그 특수 형식에 대한 호출이 됩니다. 각 형식에 대한 설명은 Special Forms에서 개별적으로 다뤄져요.
  • 매크로는 형식을 다루는 함수로, 구문적 추상화(syntactic abstraction)를 가능하게 해줘요. 호출의 연산자가 매크로 함수인 전역 var를 가리키는 심볼이면, 그 매크로 함수가 호출되는데 평가되지 않은 연산자 형식들이 인자로 전달됩니다. 매크로의 반환 값은 그 자리에서 평가돼요.
  • 연산자가 특수 형식도 매크로도 아니면 함수 호출로 간주됩니다. 연산자와 연산자들(있다면)은 왼쪽에서 오른쪽으로 모두 평가돼요. 연산자를 평가한 결과는 IFn(Clojure 함수를 나타내는 인터페이스)으로 캐스팅되고, 그 위에서 invoke()가 호출되며 평가된 인자들이 전달돼요. invoke()의 반환 값이 호출 식의 값이 됩니다. 함수 호출 형식에 메타데이터가 있어도 컴파일러가 사용할 수는 있지만 결과 값의 일부가 되지 않아요.

특수 형식과 매크로는 인자를 평범한 방식과 다르게 평가할 수 있다는 점을 기억해 두세요. 이는 Special Forms 항목에서 각각 설명됩니다.

위에서 다룬 대상들 외의 객체는 자기 자신으로 평가돼요.

load 계열 함수

(load 클래스패스-리소스 ...)
(load-file 파일명)
(load-reader reader)
(load-string 문자열)

위 내용은 하나의 형식 평가를 설명한 거예요. 반면 이 load 계열 함수들은 소스에 들어 있는 형식 묶음을 순서대로 읽고 평가합니다. 그런 형식 묶음은 보통 부수 효과를 일으키는데, 전역 환경에 영향을 주며 함수 등을 정의하기도 해요.

load 함수들은 임시 컨텍스트에서 실행되는데, 그 안에서 *ns*가 새 바인딩을 갖습니다. 즉 어떤 형식이 그 var(예: in-namespace)에 영향을 줬다 하더라도, 그 효과는 load가 끝나는 시점에 되감깁니다. load와 그 계열 함수들은 마지막 식이 만들어낸 값을 반환해요.

eval

(eval form)

eval은 형식(form) 데이터 구조를 (텍스트가 아니라!) 평가하고 결과를 돌려줍니다.

(eval (list + 1 2 3))
-> 6

(list + 1 2 3)이 먼저 평가되어 (+ 1 2 3)이라는 리스트 데이터 구조가 만들어지고, 이 구조가 다시 평가되어 6이 나오는 거죠.

더 알아보기