clojure 1.11.0-rc1 변경점
Table of Contents
Clojure 1.11.0-rc1 이 나왔다. https://github.com/clojure/clojure/blob/master/changes.md
간략하게 뭐가 바꼈나 보자.
1 키워드 인자 기능 향상
Clojure는 함수를 키워드를 받는 함수를 디자인할 때 두 가지를 선택해야한다.
1.1 key-value 쌍을 인자로 나열하기.
(defn configure [val & {:keys [debug verbose]
			:or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))
(configure 10)
;;val = 10  debug = false  verbose = false
(configure 5 :debug true)
;;val = 5  debug = true  verbose = false
;; Note that any order is ok for the kwargs
 (configure 12 :verbose true :debug true)
;;val = 12  debug = true  verbose = true
이렇게 key-value 쌍을 넣으면 저절로 바인딩 된다. 하지만 key-value 라고 아래처럼 사용할 수는 없다.
(configure 12 {:debug true})
;; IllegalArgumentException
;; No value supplied for key: {:debug true}
1.2 key-value 쌍을 hashmap 하나만 받아서 사용하기.
(defn configure [val {:keys [debug verbose] 
		      :or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))
(configure 12 {:debug true})
;;val = 12  debug = true  verbose = false
하지만 이것은 아래처럼 쓸 수 없다.
(configure 12 :debug true) ;; Wrong number of args (3) passed to:
1.3 위 두개를 합칠 순 없을까?
Clojure 1.11.0 부터는 가능하다. 의존성을 추가하자.
{:path ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.0-rc1"}}}
(defn configure [val & {:keys [debug verbose]
			:or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))
(configure 10)
(configure 10 :debug true)
(configure 10 {:debug true})
이렇게 가능해졌다. 이제 둘 중에 하나를 선택할 필요가 없어졌다. 둘다 지원하자.
참고자료
2 update-keys, update-vals
clojure 자료구조를 다루다보면, 해시맵의 key만 바꾸거나, value만 순회하면서 바꾸는 일이 비일비재하다.
update-keys , update-vals 는 이런 경우를 위해 추가된 함수들이다.
구현을 한번 보자.
(defn update-vals 
  "m f => {k (f v) ...}
  Given a map m and a function f of 1-argument, 
  returns a new map where the keys of m are mapped 
  to result of applying f to the corresponding values of m."
  {:added "1.11"}
  [m f]
  (with-meta
    (persistent!
      (reduce-kv (fn [acc k v] (assoc! acc k (f v)))
		 (if (instance? clojure.lang.IEditableCollection m)
		   (transient m)
		   (transient {}))
		 m))
    (meta)))
(defn update-keys
  "m f => {(f k) v ...}
  Given a map m and a function f of 1-argument, returns a new map whose
  keys are the result of applying f to the keys of m, mapped to the
  corresponding values of m.
  f must return a unique key for each key of m, else the behavior is undefined."
  {:added "1.11"}
  [m f]
  (let [ret (persistent!
	     (reduce-kv (fn [acc k v] (assoc! acc (f k) v))
			(transient {})
			m))]
    (with-meta ret (meta m))))
3 uuid 만들기
원래 clojure에서 UUID를 만들 때는 곧바로 자바의 java.util.UUID 생성자로 생성했다.
(java.util.UUID/randomUUID) => #uuid "b8603191-cd93-47d5-abd6-8bf2c0fbb036"
그런데 사실 clojure에는 uuid? 라는 함수는 있지만 uuid는 자바로 만들어야 했다.
1.11.0 이 되면서 random-uuid 함수가 코어로 들어왔다.
(random-uuid) => #uuid "65fb9636-814c-4a9f-bf9e-22c1abd7d039"
4 기본 값 파싱 함수들
현재까지 이 함수가 없었던 것에 놀랍다.
이번에 추가된 함수는 parse-long, parse-double, parse-boolean, parse-uuid 이다.
왜 parse-int 는 없지? 
라고 생각 할 수 있다. 하지만 clojure는 기본적으로 숫자값을 Long을 사용한다.
(type 123) => java.lang.Long
위 코드가 없을 때는 java에 있는 메소드를 이용했는데 걸리적 거리는 점은 아래와 같다.
- integer parsing: Integer/parseInt (992), Integer/valueOf (98), Integer. (270),Long/parseLong (492), Long/valueOf (66), Long. (75)
- double parsing: Double/parseDouble (186), Double/valueOf (25), Double. (23), Float/parseFloat (52), Float/valueOf (3)
- uuid parsing: UUID/fromString (134)
- boolean parsing: Boolean/parseBoolean (67)
위 내용은 깃허브에서 Clojure 코드가 JDK 함수를 파싱할 때 사용한 메소드 카운트이다. 확실히 뭔가 파편화됨을 알 수 있다.
또한 한가지 걸리는 점은 예외이다. 클로저에서는 값이 유효하지 않은 경우 예외를 던지는 것보다는 nil 을 리턴하는게 일반적이다.
하지만 자바 메소드는 대부분 NumberFormatException 예외를 던진다.
이번에 추가된 코드는 NumberFormatException 을 캐치해서 nil 을 리턴한다.
아래는 이번에 추가된 parse-long 소스코드다.
(defn parse-long
  (if (string? s)
    (try
      (Long/valueOf s)
      (catch NumberFormatException _ nil))
    (throw (IllegalArgumentException. (parsing-err s))))))
참고자료
5 core에 새로운 숫자 관련 함수 추가
abs 가 추가되었다. clojure타입에 최적화되어 있다고 한다. (long, double, ratio, bigint, bigdecimal)
(abs -1) ; 1 (abs -1/2) ; 1/2 (abs -1.1) ; 1.1 (abs -1.0M) ; 1.0M (abs (bigint "-1000000000000000000000000000000000000000000000000000000000000000000000000000")) ; => 1000000000000000000000000000000000000000000000000000000000000000000000000000N
NaN , infinite? 같은 경우는 자바 메서드 래퍼다
(NaN? (/ (* 10 0.0) 0.0)) (infinite? (/ 10 0.0))
6 clojure.math
java의 math 패키지 wrapper clojure.math 가 런칭되었다.
 
Figure 1: clojure.math
7 interation
clojure는 iteration이라는 새로운 추상화를 제시한다. iteration에 대해서는 나중에 제대로 다뤄보자. 아래는 리치히키의 코드이다.
(defn iteration
  "creates a seqable/reducible given step!,
   a function of some (opaque continuation data) k
   step! - fn of k/nil to (opaque) 'ret'
   :some? - fn of ret -> truthy, indicating there is a value
	   will not call vf/kf nor continue when false
   :vf - fn of ret -> v, the values produced by the iteration
   :kf - fn of ret -> next-k or nil (will not continue)
   :initk - the first value passed to step!
   vf, kf default to identity, some? defaults to some?, initk defaults to nil
   it is presumed that step! with non-initk is unreproducible/non-idempotent
   if step! with initk is unreproducible it is on the consumer to not consume twice"
  [step! & {:keys [vf kf some? initk]
	    :or {vf identity
		 kf identity
		 some? some?
		 initk nil}}]
  (reify
   clojure.lang.Seqable
   (seq [_]
	((fn next [ret]
	   (when (some? ret)
	     (cons (vf ret)
		   (when-let [k (kf ret)]
		     (lazy-seq (next (step! k)))))))
	 (step! initk)))
   clojure.lang.IReduceInit
   (reduce [_ rf init]
	   (loop [acc init
		  ret (step! initk)]
	     (if (some? ret)
	       (let [acc (rf acc (vf ret))]
		 (if (reduced? acc)
		   @acc
		   (if-let [k (kf ret)]
		     (recur acc (step! k))
		     acc)))
	       acc)))))
참고자료
8 as-alias
이것은 clojure의 spec을 만들 때 요긴하게 쓸 수 있다.
우리는 spec을 만들 때 qualified keyword 를 쓴다. 즉, 키워드이름에 네임스페이스까지 넣어서 사용하는 것이다. 그런데 때로 현재 네임스페이스가 아니라 좀 더 namespace에 이 도메인의 의미를 넣어주고 싶을 때가 있을 수 있다. 혹은 파일은 있지만 나는 해당 함수들을 로드하고 싶은 것이 아니라, 해당 키워드만 쓰고 싶은 것이다.
이경우 as-alias 를 쓰면 간단하게 된다. 
이것이 없는 경우 create-ns 으로 런타임에 로딩없이 네임스페이스를 만들 수 있다. 그 다음에 alias를 하는 것이다.
아래 코드를 보자.
(alias 'dm (create-ns 'my.real.domain)) (def user ::dm/user) user ;; => :my.real.domain/user
현재 나의 프로젝트에는 my.real.domain 라는 네임스페이스는 없다.
이번에 추가된 :as-alias 를 추가하자
(ns demo (:require [my.real.domain :as-alias d])) (def user2 ::d/user) user2 ;; => :my.real.domain/user
참고자료