이 문서를 읽기 위한 준비물: 타입 조사 방법

classtype 함수를 사용해 타입을 조사할 수 있다. 두 함수는 doc 함수로 조사해 보면 다음과 같은 차이점이 있다.

(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

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

컴퓨터에 익숙하다면 [[/floating-point#01--02--030000000000000004-%EB%AC%B8%EC%A0%9C]]{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

유리수

유리수를 만드는 가장 쉬운 방법은 / 표기법을 쓰거나 / 함수를 사용하는 것이다.

1/5     ;; => 1/5
(/ 1 3) ;; => 1/3

(type 1/3)     ;; => clojure.lang.Ratio
(type (/ 1 3)) ;; => clojure.lang.Ratio

다음 세 표현식은 모두 똑같이 1/3로 평가된다.

(+ (/ 1 6) (/ 1 6)) ;; => 1/3
(+ 1/6 (/ 1 6))     ;; => 1/3
(+ 1/6 1/6)         ;; => 1/3

또는 rationalize 함수를 써도 된다.

; 유리수가 아님
1.07e-20 ;; => 1.07E-20
(type 1.07e-20) ;; => java.lang.Double

; 유리수
(rationalize 1.07e-20) ;; => 107/10000000000000000000000
(type (rationalize 1.07e-20)) ;; => clojure.lang.Ratio

; 루트 2
(rationalize (Math/sqrt 2)) ;; => 14142135623730951/10000000000000000

; PI
(rationalize Math/PI) ;; => 3141592653589793/1000000000000000

몇 번 사칙연산을 해보면 편리하게 자동으로 약분이 된다는 것도 알 수 있다.

(+ 1/6) (/ 1 6)) ;; => 1/3
(+ (/ 1 6) (/ 8 6)) ;; => 3/2

유리수 검사 함수는 ratio?가 있는데 rational?과 이름이 헷갈리므로 주의해야 한다.

(type 22/7)      ;; => clojure.lang.Ratio
(ratio? 22/7)    ;; => true
(rational? 22/7) ;; => true

(type 22)        ;; => java.lang.Long
(ratio? 22)      ;; => false
(rational? 22)   ;; => true

(type 2.2)       ;; => java.lang.Double
(ratio? 2.2)     ;; => false
(rational? 2.2)  ;; => false

(type 2.2M)      ;; => java.math.BigDecimal
(ratio? 2.2M)    ;; => false
(rational? 2.2M) ;; => true

(type 22222222222222222222222222222222N)      ;; => clojure.lang.BigInt
(ratio? 22222222222222222222222222222222N)    ;; => false
(rational? 22222222222222222222222222222222N) ;; => true
  • ratio?: 주어진 수가 clojure.lang.Ratio 타입인 경우에만 true를 리턴한다.
  • rational?: 주어진 수가 부동소수점 수가 아니라면 true를 리턴한다.
type ratio? rational?
java.lang.Long false true
java.lang.Integer false true
java.lang.Double false false
java.lang.Float false false
java.math.BigDecimal false true
clojure.lang.BigInt false true
clojure.lang.Ratio true true

분자와 분모는 각각 numerator, denominator 함수로 얻을 수 있다.

(numerator 3/2)   ;; => 3
(denominator 3/2) ;; => 2

타입

promotion

Long이나 Double의 경계를 넘나드는 수나 연산을 다루면 자동으로 프로모션이 발생한다.

(def a-num 1)
(type a-num) ;; => java.lang.Long

(def b-num (+ a-num 99999999999999999999)) ;; => 100000000000000000000N
(type b-num) ;; => clojure.lang.BigInt

(def c-num (+ a-num 99999999999999999999.8)) ;; => 1.0E20
(type c-num) ;; => java.lang.Double

(def d-num (+ a-num 99999999999999999999.8M)) ;; => 100000000000000000000.8M
(type d-num) ;; => java.math.BigDecimal

overflow

Java에서 Long.MAX_VALUE1을 더하면 다음과 같이 오버플로우가 발생한다.

long num = Long.MAX_VALUE + 1;
// -9223372036854775808

그러나 Clojure에서는 ArithmeticException 예외가 던져진다.

(+ Long/MAX_VALUE 1)

; Execution error (ArithmeticException) at ... (REPL:57).
; integer overflow

정밀도

truncation

;                               ↓                           ↓
(def a-number 0.123456789123456789) ;; => 0.12345678912345678
(def b-number 0.123456789123456780) ;; => 0.12345678912345678

(= a-number b-number) ;; => true
(type a-number) ;; => java.lang.Double
(type b-number) ;; => java.lang.Double

위의 예제를 보면 a-number의 마지막 9가 절삭(truncation)되어 정밀도가 떨어졌음을 알 수 있다.

따라서 a-number를 더 작은 수인 b-number=로 비교하면 true로 평가된다.

정밀도를 보장하려면 수 마지막에 M을 붙여서 java.math.BigDecimal 타입으로 생성해 주어야 한다.

(def a-number 0.123456789123456789M) ;; => 0.123456789123456789M
(type a-number) ;; => java.math.BigDecimal

부동소수점과 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]])