2010/10/04 00:27
Clojure를 선택하기 어렵게 만드는 것들? - 상태 변화, 객체 지향 시스템 Lisp2010/10/04 00:27
사람들이 Lisp을 처음 배울 때 Clojure를 선택하기 어렵게 만드는 요인들이 뭐가 있을까요? 일단 '상태를 변화시키는 게 번거롭다. 불가능하진 않은데, 뭔가 귀찮다.'라는 것도 하나가 되겠죠. 상태 변화를 지양하는 게 바람직하긴 하지만, 그게 너무 불편해 언어 배우는 거 자체가 싫어진다면, 방법은 있습니다. 아래와 같은 variable 함수를 짜면 되겠죠.
(defn variable [& initial-value]
(let [value (atom (or nil (first initial-value)))]
(fn [& keyword-and-new-value]
(if (empty? keyword-and-new-value)
(deref value)
(let [keyword (first keyword-and-new-value)
new-value (second keyword-and-new-value)]
(cond (= keyword :=) (reset! value new-value)
(= keyword :value) (deref value)))))))
이 함수를 이용하면, let이나 def로 정의한 변수의 값을 '간편하게' 변경할 수 있습니다. 다음 테스트케이스를 보시죠. let에 대한 예이지만 def에도 동일하게 사용하시면 됩니다.
(deftest variable-test
(let [x (variable)
y (variable 3)
z (variable {:a 1 :b 2})]
(is (nil? (x)))
(is (= 3 (y)))
(is (= 3 (y :value)))
(is (= 4 (y := 4)))
(is (= 4 (y)))
(is (= 5 (y := (+ 1 (y)))))
(is (= 5 (y)))
(is (= {:a 1 :b 2} (z)))
(is (= {:a 1 :b 2 :c 3} (z := (assoc (z) :c 3))))
(is (= {:a 1 :b 2 :c 3} (z)))
(is (= {:a 1 :b 2 :c 3} (z :value)))))
사용법은 아래와 같습니다.
(def x (variable)) ; x를 변경 가능한 값으로 만듭니다.
(def x (variable 3)) ; x를 초기값이 3인, 변경 가능한 값으로 만듭니다.
(x) ; x의 값을 리턴합니다. 위와 같이 설정했다면 3이 되겠죠.
(x :value) ; 역시 x의 값을 리턴합니다. (x)와 똑같은 기능을 합니다. 다만 (x)와 같이 쓸 경우, 가독성이 안 좋은 경우가 있어, :value란 값을 넘기면 값을 리턴하도록 만들어 놓은 것입니다. 취향대로 쓰시면 됩니다.
(x := 4) ; :=는 값을 설정하는데 사용합니다. 왼쪽과 같이 하면 x의 값은 4가 되겠죠.
또 장애가 되는 게 뭐가 있을까요? 객체 지향 언어가 아니라는 게 마음에 걸리시나요? Common Lisp 객체 지향 시스템의 핵심이 어떤 거라고 생각하시나요? 바로 '멀티메서드'입니다. (Common Lisp의 Object System은 일반적으로 사람들이 생각하는 것과 조금 다릅니다.) 그리고 Clojure 역시 멀티메서드 시스템을 가지고 있죠. dispatch 함수와 derive를 통해, type을 정하고, type 사이의 관계를 정의하는 것도 가능하구요. 따라서 객체 지향 시스템이 추구하는 '다형성'은 기본적으로 가능합니다. 그럼 뭐만 있으면 될까요? inheritance만 있으면 되겠죠.(사실 폴 그레이엄의 글을 보면 알 수 있듯이 객체 지향 시스템이 갖춰야 하는 요건이 무엇인지는 약간 애매한 문제입니다. encapsulation 같은 부분도 좀 그렇지만.. encapsulation의 한 방법으로는 closure를 사용할 수 있겠죠.) 이 부분은 그리 어렵지 않으니 스스로 한 번 해보시기 바랍니다.
아니면, Lisp의 장기를 살려, 객체 지향 시스템을 만들어 볼 수도 있겠죠. 아래 제가 Smalltalk의 시스템을 본따서 Clojure로 만든 객체 지향 시스템을 첨부합니다. 사실 예전에 올렸던, ANSI Common Lisp 책의 예제를 Clojure로 구현한 객체 지향 시스템은 여러가지 문제가 있었습니다. 메시지를 보낼 때마다 tell을 호출해야 하니, 순식간에 코드는 tell로 뒤덮이게 될 것이고, 이래서야 도저히 실제로 사용할 수가 없겠죠. 그 밖에도 인스턴스 변수 값을 변경할 수 없고, 클래스 변수와 예약어 'super'가 없으며, 객체들이 메서드를 따로 가지고 있어서 공간 낭비가 크다는 단점도 있었습니다. 이번에 짠 코드는 이런 단점들을 모두 개선했습니다. 사용법의 예는 아래와 같이 (object :message & arguments ...)와 같은 문법을 따릅니다. Smalltalk에서는 모든 클래스가 Object에 subclass 메시지를 보내서 만들어지기 때문에, 아래와 같이 클래스를 선언하게 됩니다. (Object가 java.lang.Object를 가리키고 있기 때문에, 이 라이브러리에서는 모두 대문자인 OBJECT로 이름을 붙였습니다.)
(def Rectangle
(OBJECT :subclass
{:class 'Rectangle
:instance-variable-names [:width :height]
:area (fn [] (* (self :width) (self :height)))}))
(let [rectangle (Rectangle :new {:width 20 :height 30})]
(rectangle :area))
=> 600
예약어 super는 self와 똑같이 사용하시면 됩니다. 보시다시피 모든 inheritance는 :subclass 메시지를 통해 이루어지구요. 자세한 사용법은 유닛테스트를 참조하시면 좋을 것 같습니다.
(defn variable [& initial-value]
(let [value (atom (or nil (first initial-value)))]
(fn [& keyword-and-new-value]
(if (empty? keyword-and-new-value)
(deref value)
(let [keyword (first keyword-and-new-value)
new-value (second keyword-and-new-value)]
(cond (= keyword :=) (reset! value new-value)
(= keyword :value) (deref value)))))))
이 함수를 이용하면, let이나 def로 정의한 변수의 값을 '간편하게' 변경할 수 있습니다. 다음 테스트케이스를 보시죠. let에 대한 예이지만 def에도 동일하게 사용하시면 됩니다.
(deftest variable-test
(let [x (variable)
y (variable 3)
z (variable {:a 1 :b 2})]
(is (nil? (x)))
(is (= 3 (y)))
(is (= 3 (y :value)))
(is (= 4 (y := 4)))
(is (= 4 (y)))
(is (= 5 (y := (+ 1 (y)))))
(is (= 5 (y)))
(is (= {:a 1 :b 2} (z)))
(is (= {:a 1 :b 2 :c 3} (z := (assoc (z) :c 3))))
(is (= {:a 1 :b 2 :c 3} (z)))
(is (= {:a 1 :b 2 :c 3} (z :value)))))
사용법은 아래와 같습니다.
(def x (variable)) ; x를 변경 가능한 값으로 만듭니다.
(def x (variable 3)) ; x를 초기값이 3인, 변경 가능한 값으로 만듭니다.
(x) ; x의 값을 리턴합니다. 위와 같이 설정했다면 3이 되겠죠.
(x :value) ; 역시 x의 값을 리턴합니다. (x)와 똑같은 기능을 합니다. 다만 (x)와 같이 쓸 경우, 가독성이 안 좋은 경우가 있어, :value란 값을 넘기면 값을 리턴하도록 만들어 놓은 것입니다. 취향대로 쓰시면 됩니다.
(x := 4) ; :=는 값을 설정하는데 사용합니다. 왼쪽과 같이 하면 x의 값은 4가 되겠죠.
또 장애가 되는 게 뭐가 있을까요? 객체 지향 언어가 아니라는 게 마음에 걸리시나요? Common Lisp 객체 지향 시스템의 핵심이 어떤 거라고 생각하시나요? 바로 '멀티메서드'입니다. (Common Lisp의 Object System은 일반적으로 사람들이 생각하는 것과 조금 다릅니다.) 그리고 Clojure 역시 멀티메서드 시스템을 가지고 있죠. dispatch 함수와 derive를 통해, type을 정하고, type 사이의 관계를 정의하는 것도 가능하구요. 따라서 객체 지향 시스템이 추구하는 '다형성'은 기본적으로 가능합니다. 그럼 뭐만 있으면 될까요? inheritance만 있으면 되겠죠.(사실 폴 그레이엄의 글을 보면 알 수 있듯이 객체 지향 시스템이 갖춰야 하는 요건이 무엇인지는 약간 애매한 문제입니다. encapsulation 같은 부분도 좀 그렇지만.. encapsulation의 한 방법으로는 closure를 사용할 수 있겠죠.) 이 부분은 그리 어렵지 않으니 스스로 한 번 해보시기 바랍니다.
아니면, Lisp의 장기를 살려, 객체 지향 시스템을 만들어 볼 수도 있겠죠. 아래 제가 Smalltalk의 시스템을 본따서 Clojure로 만든 객체 지향 시스템을 첨부합니다. 사실 예전에 올렸던, ANSI Common Lisp 책의 예제를 Clojure로 구현한 객체 지향 시스템은 여러가지 문제가 있었습니다. 메시지를 보낼 때마다 tell을 호출해야 하니, 순식간에 코드는 tell로 뒤덮이게 될 것이고, 이래서야 도저히 실제로 사용할 수가 없겠죠. 그 밖에도 인스턴스 변수 값을 변경할 수 없고, 클래스 변수와 예약어 'super'가 없으며, 객체들이 메서드를 따로 가지고 있어서 공간 낭비가 크다는 단점도 있었습니다. 이번에 짠 코드는 이런 단점들을 모두 개선했습니다. 사용법의 예는 아래와 같이 (object :message & arguments ...)와 같은 문법을 따릅니다. Smalltalk에서는 모든 클래스가 Object에 subclass 메시지를 보내서 만들어지기 때문에, 아래와 같이 클래스를 선언하게 됩니다. (Object가 java.lang.Object를 가리키고 있기 때문에, 이 라이브러리에서는 모두 대문자인 OBJECT로 이름을 붙였습니다.)
(def Rectangle
(OBJECT :subclass
{:class 'Rectangle
:instance-variable-names [:width :height]
:area (fn [] (* (self :width) (self :height)))}))
(let [rectangle (Rectangle :new {:width 20 :height 30})]
(rectangle :area))
=> 600
예약어 super는 self와 똑같이 사용하시면 됩니다. 보시다시피 모든 inheritance는 :subclass 메시지를 통해 이루어지구요. 자세한 사용법은 유닛테스트를 참조하시면 좋을 것 같습니다.
object-system.zip
