[220611] Clojure macro로 만드는 기이한 생각
1 소개
조회하는 기능과 수정하는 기능은 하는 일은 비슷한데 딱 한가지 값을 가져오냐 넣느냐의 차이만 있는 것처럼 보인다. 마치 주소는 하나인데 들어올 수 있는 문 따로 나가는 문은 따로 만들어 놓는 것이다.
물론 하나의 일만 하면 좋을 수 있지만 입구가 출구가 되지 못한다는 것이 과연 편리한 일일까 라는 생각을 해본다.
오늘은 그래서 매크로를 이용해서 문을 하나로 합치는 시도를 해보려고 한다.
거기다가 한가지 anaphoric macro
의 개념까지 더했다. 이정도면 꽤나 흥미로운 코드가 될 거라고 생각한다.
2 첫번째 코드
(defmacro ! [find-expr value] (let [op (first find-expr) operand-1 (second find-expr) operands (rest find-expr)] (cond (and (map? operand-1) (keyword? op)) (let [path op] `(let [~'it ~find-expr] (assoc ~operand-1 ~path ~value))) (= 'get op) (let [path (nth find-expr 2)] `(let [~'it ~find-expr] (assoc ~operand-1 ~path ~value))) (= 'get-in op) (let [path (nth find-expr 2)] `(let [~'it ~find-expr] (assoc-in ~operand-1 ~path ~value))) :else (throw (ex-info "잘못된 패턴" {:find-expr find-expr :op op}))))) ;; 사용방법 (! (:a {:a 1}) 100) ;; => {:a 100} (! (:a {:a 1}) (+ it 100)) ;; => {:a 101} (! (get [1 2 3] 2) 100) ;; => [1 2 100] (! (get [1 2 3] 2) (+ it 100)) ;; => [1 2 102] (def m {:profile {:name "프로필명"}}) (! (get-in m [:profile :name]) "영환") ;; => {:profile {:name "영환"}} (! (get-in m [:profile :name]) (str "남영환의 " it)) ;; => {:profile {:name "남영환의 프로필명"}}
3 더 나아가기
이것만으로 흥미롭지만 나에게는 한가지 더 해보고 싶은 것이 있다.
바로 키워드는 조회되는데 왜 숫자는 안되는가
이다.
그러므로 나는 숫자, 문자열도 get
을 사용하지 않고 키워드를 사용한 것처럼 수정하면 좋겠다는 생각을 했다.
(유효성검증은 모두 무시할 것. 매크로 확장을 하면서 컴파일타임에 문제를 해결할 수 있다고 생각한다)
즉, 아래처럼 쓰고 싶다는 뜻이다.
(def a [1 2 3]) (! (1 a) "A") ;; => [1 "A" 3] (def str-map {"hi" "hello"}) (! ("hi" str-map) "bye") ;; => {"hi" "bye"}
다음 코드를 추가함으로써 완성된다.
(number? op) (let [path operand-1] `(! (~'get ~path ~op) ~value)) (string? op) (let [path operand-1] `(! (~'get ~path ~op) ~value))
4 완성된 코드
(defmacro ! [find-expr value] (let [op (first find-expr) operand-1 (second find-expr) operands (rest find-expr)] (cond (and (map? operand-1) (keyword? op)) (let [path op] `(let [~'it ~find-expr] (assoc ~operand-1 ~path ~value))) (= 'get op) (let [path (nth find-expr 2)] `(let [~'it ~find-expr] (assoc ~operand-1 ~path ~value))) (= 'get-in op) (let [path (nth find-expr 2)] `(let [~'it ~find-expr] (assoc-in ~operand-1 ~path ~value))) (number? op) (let [path operand-1] `(! (~'get ~path ~op) ~value)) (string? op) (let [path operand-1] `(! (~'get ~path ~op) ~value)) :else (throw (ex-info "잘못된 패턴" {:find-expr find-expr :op op})))))