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 가 런칭되었다.

math.png

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

참고자료

Date: 2021-02-17 Wed 00:00

Author: Younghwan Nam

Created: 2022-11-15 Tue 08:10

Emacs 27.2 (Org mode 9.4.4)

Validate