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
참고자료