number category

Clojure는 다음과 같이 수 카테고리를 분류한다.1

  • 정수, 분수(비율)
    • 정수: Java의 java.lang.Longjava.lang.Integer. 기본값은 Long.
    • 분수: clojure.lang.Ratio.
  • 부동소수점 수
    • Java의 java.lang.Floatjava.lang.Double. 기본값은 Double.
  • BigDecimal
    • Java의 java.math.BigDecimal.

동등성 비교

수의 동등성 비교에는 =, ==를 사용한다.

Clojure의 =는 수의 크기뿐 아니라 카테고리도 같아야 true를 리턴하므로 카테고리를 생각하며 사용해야 한다.

; 카테고리가 같은 경우
(= 1 2/2)   ; true   정수와 분수
; 카테고리가 다른 경우
(= 1.0 1)   ; false   Double과 Long
(= 1M 1)    ; false   BigDecimal과 Long
(= 1/2 0.5) ; false   Ratio와 Double

==는 수의 크기 비교 전용이며, 카테고리를 무시한다. Java의 ==처럼 주소 비교가 아니라는 점에 주의할 것.

(== 1.0 1) ; true
(== 1M 1)  ; true
(== 2/2 1) ; true
(== 1/4 0.25M) ; true
(== 1/4 0.25) ; true

컴퓨터에 익숙하다면 0.1 + 0.2 문제를 알고 있을 것이다. 이 문제는 [[/floating-point]]{부동소수점}의 특성과 관련되어 있다. 그러므로 다음 예제처럼 작업한다면 =, == 둘 중 무엇을 써도 컴퓨터 바깥 세상의 수학과 같은 결과가 나오지 않는다.

(+ 0.1 0.2) ; 0.30000000000000004

(== (+ 0.1 0.2) 0.3) ; false
(= (+ 0.1 0.2) 0.3)  ; false

그래서 이런 경우엔 분수를 사용하거나 BigDecimal을 사용해야 원하는 결과가 나온다.

(= (+ 1/10 2/10) 3/10)  ; true
(== (+ 1/10 2/10) 3/10) ; true

(= (+ 0.1M 0.2M) 0.3M)  ; true
(== (+ 0.1M 0.2M) 0.3M) ; true

크기 비교

Clojure도 Java처럼 <, <=, >, >=를 사용한다.

(< 1 2)  ; true   1 <  2
(<= 1 2) ; true   1 <= 2
(> 1 2)  ; false  1 >  2
(>= 1 2) ; false  1 >= 2

Java에 익숙한 상태에서 이런 비교 코드를 접하면 굉장히 생소하게 보일 수 있다. 하지만 이 표기법은 3개 이상의 수를 비표할 때 상당한 편리함이 있다.

(< 3 4 5 6)   ; true
(< 3 4 5 1 8) ; false

변수 x가 특정 범위에 있는지 검사할 때 Java였다면 다음과 같이 작업했을 것이다.

if (10 <= x && x <= 20) {
    // ...
}

하지만 Clojure 에서는 이렇게 하면 된다.

(if (<= 10 x 20)
    ; ...
)

사칙연산

Clojure는 Lisp 방언이므로 전위 표기법(prefix notation)을 사용한다. Java 코드와 비교해 보자.

Java Clojure
1 + 2 (+ 1 2)
1 + (2 * 3) (+ 1 (* 2 3))
1 + 2 + 3 (+ 1 2 3)
2 * 3 * 4 (* 2 3 4)
5 - 2 (- 5 2)
5 / 2 (quot 5 2)
5 % 2 (rem 5 2)

Clojure는 Ratio 타입을 기본으로 지원하고 있기 때문에 나눗셈 연산자를 그대로 사용하면 분수로 평가된다는 점을 기억해둘 필요가 있다.

(quot 9 4)  ; 2    몫
(rem 9 4)   ; 1    나머지
(/ 9 4)     ; 9/4  비율, 분수

(class (/ 9 4))   ; 9/4의 타입은 clojure.lang.Ratio
(type (/ 9 4))    ; 9/4의 타입은 clojure.lang.Ratio

마지막 줄에 주목. classtype을 사용해 타입을 알아낼 수 있다는 것도 기억해두자.

타입 조사하기

classtype이 똑같이 작동하는 것으로 보이는데 REPL에서 (doc type), (doc class)로 조사해 보니 다음과 같은 차이점이 있었다.

(doc type)
-------------------------
clojure.core/type
([x])
  Returns the :type metadata of x, or its Class if none
=> nil

(doc class)
-------------------------
clojure.core/class
([x])
  Returns the Class of x
=> nil
  • type: 주어진 값의 metadata에 들어있는 :type을 리턴한다. 만약 :type이 없다면 Class를 리턴한다.
  • class: 주어진 값의 Class를 리턴한다.

숫자의 타입을 조사해보면 Clojure는 기본적으로 DoubleLong을 사용하고 있다는 것을 확인할 수 있다.

(type 1.0)  ; java.lang.Double
(type 1)    ; java.lang.Long

부동소수점과 BigDecimal

앞에서 언급했던 0.1 + 0.2 문제를 다시 살펴보자.

(+ 0.1 0.2) ; 0.30000000000000004

예상과 같다. 이걸 정확히 계산하려면 Java의 BigDecimal 타입을 사용하면 되는데, Clojure에서는 숫자에 postfix로 M을 붙이면 된다.

(+ 0.1M 0.2M)   ; 0.3M
(class *1)      ; java.math.BigDecimal

여기에서 *1은 REPL에서 가장 최근에 평가한 결과를 의미한다. 그 이전은 *2.

만약 이미 정의된 숫자를 사용해 BigDecimal 타입의 인스턴스를 생성하려면 다음과 같이 하면 된다.

(bigdec "1.0")  ; 1.0M
(bigdec 1.0)    ; 1.0M
(bigdec 0.1)    ; 0.1M

Java의 BigDecimal 클래스의 생성자를 사용하고 싶다면 그냥 new 키워드를 사용해도 된다.

(new BigDecimal "1.0")  ; 1.0M
(new BigDecimal "0.1")  ; 0.1M
(new BigDecimal 0.1)    ; 0.1000000000000000055511151231257827021181583404541015625M

마지막 줄의 0.1 문제는 0.1을 부동소수점으로 표현할 때의 문제이므로 Java에서 new BigDecimal(0.1)을 쓰면 똑같은 결과가 나온다. 즉 Clojure의 문제는 아니다.

다만 (bigdec 0.1)0.1M 이었지만 (new BigDecimal 0.1)0.1M이 아니라는 것은 주목해야 한다.

bigdecnew BigDecimal을 단순하게 래핑한 함수가 아니라는 것. 사용하게 되면 꼭 테스트 코드를 통해 실험해 보도록 하자.

거듭제곱

거듭제곱은 어떻게 표현할 수 있을까?

Javascript라면 Math.pow(x, y)를 사용하면 된다.[^javascript-exponentation]

Math.pow(2, 10);    // 1024

이건 Python이 가장 쉽다. 거듭제곱 전용 연산자가 있기 때문이다.

2 ** 10;    # 1024

Clojure에서는 거듭제곱 연산자를 기본으로 지원하지는 않는 것 같다. 그래서 다음과 같은 함수를 만들어 쓰면 될 것 같다.

(defn power [x n]
  (reduce * (repeat n x)))

(power 2 10)  ; 1024

*도 함수 이름에 쓸 수 있는 문자이기 때문에 이렇게 할 수도 있다.[^clojure-star-star]

(defn ** [x n]
  (reduce * (repeat n x)))

(** 2 10)  ; 1024

(repeat 10 2)(2 2 2 2 2 2 2 2 2 2)를 생산하므로, 이 결과에 *reduce하는 코드라 할 수 있다.

기본 연산자가 제공되지 않는 건 아쉽지만 reduce-*-repeat이라는 코드를 작성할 수 있다는 것이 재미있다.

Javascript에서 Math.pow를 사용하지 않고 굳이 비슷한 스타일의 코드를 작성한다면 다음과 같이 할 수 있겠다.

function power(x, n) {
  return Array(n).fill(x).reduce((a, b) => a * b);
}

power(2, 10); // 1024

만약 만들어 쓰는 것이 귀찮거나 어떻게 Clojure 코드로 표현할 지 생각이 안 난다면 그냥 Java의 java.lang.Mathpow를 가져다 써도 된다.

(Math/pow 2 10) ; 1024.0

Clojure는 java.lang은 기본으로 import를 해둔다고 한다. 그래서 Math/pow를 저렇게 간단하게 불러 쓸 수 있는 것 같다.

계산 예제

Double과 BigDecimal의 합은 Double.

(+ 1.0 3M)      ; 4.0
(+ 1.0 99999999999999999999999999999999999999999999999999999M) ; 1.0E53

8진수와 16진수는 모두 Long이므로 결과도 java.lang.Long.

(+ 010 0xF) ; 23
(+ 10.0 0xF)    ; 25.0   Double과 16진수 Long의 합
(+ 1/2 010)     ; 17/2   Ratio와 8진수 Long의 합

과학 표기법.

(+ 1 7.0E-10)   ; 1.0000000007
(+ 1 7.0E+10)   ; 7.0000000001E10

Ratio 타입은 자동으로 약분이 된다.

(+ 1/3 3/9) ; 2/3

참고문헌

주석

  1. Clojure Equality (한국어 번역: [[/clojure/equality]])