[220611] Clojure macro로 만드는 기이한 생각

Table of Contents

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})))))

Author: Younghwan Nam

Created: 2022-11-15 Tue 08:10

Emacs 27.2 (Org mode 9.4.4)

Validate