매크로에 대한 기억
어린 시절 즐기던 게임에서는 레벨 업을 위해 지루한 반복 사냥을 해야 할 때가 많았습니다. 제 친구 중 몇 명은 매크로 프로그램을 이용해 반복 사냥을 컴퓨터가 대신하게 했습니다. 매크로를 이용하면 캐릭터가 어떤 방향으로 공격을 하게 하거나, 몇 분에 한 번씩 아이템을 사용하도록 하는 등 일련의 작업을 자동으로 실행할 수 있었습니다. 이처럼, '매크로'란 미리 정해놓은 명령을 자동으로 실행하여 단순 반복 작업을 줄이는 기능입니다. 그런데 프로그래밍에서도 따분한 반복 작업을 해야 하는 경우가 있습니다. 프로그래밍 언어에는 일정한 문법과 규칙이 있기 때문입니다. 예를 들어, 자바에서 새 객체를 만들려면 다음과 같은 절차를 반드시 따라야 합니다.
- class 키워드로 클래스를 정의한다.
- new 키워드로 객체를 생성한다.
이 두 문장은 뒤바뀔 수 없습니다. 둘 중 하나의 행위만으로는 객체를 만들 수 없죠. 프로그래밍 언어의 문법의 틀 을 당연시한다면 생각은 그와 유사한 방식으로만 상상할 수밖에 없다고 생각합니다. 클로저(Clojure)의 매크로는 주어진 규칙을 허무는 데 도움을 줄 것입니다.
프로그래밍 언어의 매크로
C 언어의 매크로
클로저의 매크로에 관해 알아보기 전에, 더 많은 분들이 알고 계실 C 언어의 매크로를 잠시 살펴봅시다.
1#include <stdio.h>23#define CUBE_MACRO(n) n*n*n45int main()6{7 printf("%d", CUBE_MACRO(1+2)); // 1+2*1+2*1+2 -> 78 return 0;9}
위 코드에서는 #define 전처리 명령으로 n을 세제곱하는 CUBE_MACRO라는 매크로 함수를 정의했습니다. CUBE_MACRO 매크로는 일반 함수를 호출하는 것처럼 사용할 수 있습니다. 한편, 아래의 코드에서는 CUBE_MACRO 매크로의 n*n*n과 동일한 식을 일반 함수 cube_func()로 정의했습니다.
1#include <stdio.h>23int cube_func(int n)4{5 return n*n*n;6}78int main()9{10 printf("%d", cube_func(1+2)); // 3*3*3 -> 2711 return 0;12}
이 두 코드는 동일한 일을 하는 것 같지만, 결과 값이 다릅니다.[2][3] CUBE_MACRO는 7이 출력되며, cube_func는 27이 출력되었습니다. C언어 매크로는 기호(여기서는 1+2)를 다른 기호로 치환한 뒤, 코드를 컴파일하고 실행합니다. 함수는 이미 코드가 컴파일 된 상태에서, 기호를 값으로 평가한 후에(2+1 -> 3) 실행합니다. 컴파일 전에는 1+2라는 코드가 3이라는 값으로 평가되지 않으므로, n*n*n은 3*3*3이 아니라 1+2*1+2*1+2로 치환됩니다. 따라 서 매크로 함수와 일반 함수의 실행 결과에 차이가 생기는 것입니다. C 언어의 매크로는 이처럼 코드 자체를 변경하며, 이를 이용하면 제한적으로나마 기존 언어에서 정한 규칙의 선을 넘을 수 있습니다.
클로저 매크로
클로저 매크로도 C언어 매크로와 비슷하지만, 단순히 문자열 치환이 아닙니다. 코드를 프로그래밍하는 것에 가깝습니다. 코드를 치환한다는 점에서 클로저의 매크로는 C언어 매크로와 비슷하지만, 리스프 계열 언어의 특징인 동형성으로 특별해집니다. [4][5][6]
동형성
클로저는 리스프 계열 언어입니다. 리스프(LISP)라는 이름은 "리스트 처리(LISt Processing)"에서 딴 것입니다. 그 이름에서도 짐작할 수 있듯이, 연결 리스트(linked list)가 리스프의 핵심 자료구조입니다. 심지어 코드 자체도 리스트로 구성할 정도이죠. 그래서 클로저에서는 코드가 리스트 자료구조와 같은 방식으로 표현·저장됩니다. 코드를 변경하는 것이 리스트를 변경하는 것과 똑같은 일입니다. 클로저에서는 소괄호 안에 요소들을 나열하는 방식으로 리스트를 표기합니다. 다음 두 코드는 각각 자바와 클로저로 리스트를 표현한 것입니다.
1List.of(1,2,3,4)
1(list 1 2 3 4) ;; (1 2 3 4)
클로저에서는 식을 계산하거나 함수를 호출할 때 코드를 리스트로 작성합니다.
1(+ 1 2)2(* 2 3)3(/ 3 4)45(println "Hello, Clojure")
함수 정의도 마찬가지입니다.
1(defn hello-world [name]2 (println (str "Hello, " name)))34(defn add1 [number]5 (+ 1 number))
코드가 리스트 자료구조로 구성되어 있으므로, 코드를 다른 리스트를 조작할 때와 똑같이 조작할 수 있습니다.
1(count (list * 3 3 3)) ;; 42(first (list * 3 3 3)) ;; *3(rest (list * 3 3 3)) ;; (3 3 3)4(concat (list * 3 3 3) '(5)) ;; (3 3 3 5)
클로저 코드는 소괄호를 적극적으로 활용하여 문법을 최소화하였습니다. 일반적으로 리스트의 첫 번째 요소를 연산자(함수)로, 나머지 요소들을 피연산자(인자)로 취급합니다. 동형성은 매크로를 사용할 때 아주 강력한 힘이 됩니다. 우리는 '코드를 생산하는 코드'