Skip to content
kwakbab edited this page Nov 16, 2014 · 13 revisions

On Scalars

뻔한 것을 분석하기 위해서는 뻔하지 않은 마음가짐이 필요하다.

  • Alfred North Whitehead

Understanding Precision (정밀도의 이해)

클로저의 숫자는 디폴트로 그들이 필요한 만큼만 정밀.

Truncation (끝을 잘라냄)

  • truncation은 부동소수점의 표현의 한계에 대해 대한 정밀도를 제한하는 것을 말한다.
  • 숫자가 끝이 잘리면, 그 정밀도에 대한 최대 자리수는 저장공간에 적합한 비트수에 대해 제한 된다.
  • 부동 소수점의 경우, 클로저는 기본적으로 (by default) 자른다.
  • 그래서 고정밀 부동소수점 연산이 요구되는 경우, M을 사용하여 명시적으로 입력해야한다.
(let [imadeuapi 3.14159265358979323846264338327950288419716939937M]
	(println (class imadeuapi))
	imadeuapi)

;java.math.BigDecimal
=> 3.14159265358979323846264338327950288419716939937M


(let [butieatedit 3.14159265358979323846264338327950288419716939937]
          (println (class butieatedit))
          butieatedit)
;java.lang.Double
=>3.141592653589793
  • java의 디폴트인 더블 타입이 부족하기 때문에 butieatedit는 잘린다.
  • 한편 imadeuapi은 클로저의의 리터럴 표기법을 사용해, 임의의 십진 숫자임을 표현했다.
  • 이 것은 값의 광범위한 잘림을 경감하는 방법중의 하나이지만 정밀도를 보장하지는 않는다.

Promotions (승격)

  • 오버플로우가 발생하면 클로저는 감지할 수 있고, 더 큰 값에 대응할 수 있는 수치표현으로 승격한다.
  • 자주 사용하는 클래스 쌍의 사용에 있어 승격의 결과는 매우 큰 값을 유지한다.
  • This promotion in Clojure is automatic, because the primary focus is first correctness of numerical values, then raw speed.
  • 승격은 일어날 것이고 이 필연성에 대응할 필요가 있다는 것을 기억하는 것이 중요하다.
(def clueless 9)

(class clueless)
;=> java.lang.Long  ///Long 이 기본

(class (+ clueless 9000000000000000))
;=> java.lang.Long  ///  여기까진 Long이 담을 수 있는 큰 값

(class (+ clueless 90000000000000000000))
;=> clojure.lang.BigInt  //더 커지면 Bigint로 승격

(class (+ clueless 9.0))
;=> java.lang.Double //부동소수점으로 전환
  • Java는 자동 형변환이 발생하는 컨텍스트 꾸러미를 가지고 있기 때문에, 우리는 자바 네이티브 라이브러리를 다룰때 당신도 함께 익숙해 지기를 권장한다. (뭔소리)

  • 한쪽에 기댄 승격!!

Overflow

(Long/MAX_VALUE)
=> 9223372036854775807
(+ Long/MAX_VALUE Long/MAX_VALUE)

=> ArithmeticException integer overflow  clojure.lang.Numbers.throwIntOverflow (Numbers.java:1424)

Underflow

오버플로우와 반대. 값이 너무 작으면 0으로 떨어진다.

   (float 0.0000000000000000000000000000000000000000000001)
        ;=> 0.0
        1.0E-430
        ;=> 0.0

Rounding errors (todo: 여기 보충이 필요 ㅋㅋ)

  • 0.1 초마다 타이머를 등록해야하는데, 하드웨어가 0.1을 바로바로 표현하지 못함.
  • 결과적으로 100시간이 경과?
  • 0.34초
(let [approx-interval (/ 209715 2097152)
		actual-interval (/ 1 10)
		hours			(* 3600 100 10)
		actual-total (double (* hours actual-interval))
		apporx-total (double (* hours approx-interval))]
	(- actual-total apporx-total))

; => 0.34332275390625



(+ 0.1M 0.1M 0.1M 0.1 0.1M 0.1M 0.1M 0.1M 0.1M 0.1M)
;0.9999999999999999

Trying to be rational (유리수 되기)

  • 클로저는 유리수를 표현할 수 있는 데이터 타입을 제공하고, 모든 코어 수학함수들이 유리수와 함께 계산된다.
  • 유리수는 22/7(7분의 22)형식에서 임의의 정밀도 정수의 분모와 분자로 구성된 비율이다.
  • 유리수형 필요할 때 완벽한 정밀도를 유지하는 방법을 제공한다 (me.4.1을 상기해보자.)

Why be rational? (왜 유리수가 되어야 하냐)

  • 물론, 클로저는 decimal타입을 제공하고, 그것은 컴퓨터의 메모리에 대해 무한히 상대적이다.
  • BigDecimal 클래스는 유한한 32bit정수를 표현한다.
  • 이것은 큰 범위의 값을 완벽하게 표현한다. (물론 아직 에러에 대한 문제는 있다)
1.0E-430000000M
; => 1.0E-4300000000M

1.0E-4300000000M
;=> NumberFormatException   java.math.BigDecimal.<init> (BigDecimal.java:511)
  • 당신이 BigDecimal 의 값이 부동 소수점 손상 으로부터 자유롭다는 것을 보장하기 위해 관리 하는 경우에도 당신은 자신으로부터 그들을 보호할 수 없다.
  • 아직, 어느 시점부터 부동 소수점 계산은, 2/3와 같이 항상 버림이 필요해지는 미묘한 전파 오류가 발생한다.
  • 계산 순서에 따라 결과가 달라짐
(def a  1.0e50)
(def b -1.0e50)
(def c 17.0e00)

(+ (+ a b) c)
;;=> 17.0

(+ a (+ b c))
;;=> 0.0

How to be rational (어떻게 유리수가 되나)

  • rational 타입을 제외하고도, 클로저는 너의 온전함을 유지할 수 있게 도와주는 함수(ratio?, rational?, rationalize)를 제공한다.
  • rational을 분해하는 것은 역시 하찮은 문제다.
  • 계산의 정확성을 유지하는 것을 보증하는 최고의 방법은 모두 rational을 쓰는 것이다.
  • 부동소숫점을 씀으로써 결과가 지워지는 쇼킹한 것을 다음에서 볼 수 있다.
(def a (rationalize 1.0e50))
(def b (rationalize -1.0e50))
(def c (rationalize 17.0e00))

(+ (+ a b) c)
;;=> 17N 결합되어도 잘 보존

(+ a (+ b c))
;;=> 17N
  • 주어진숫자가 rational인지 확인하기위해 rational?을 쓸 수 있고, 거긋을 rationalize를 통해 변환할 수 있다.

  • 정밀도를 정확하게 지키기 위한 몇가지 기억해야할 룰

    • Bigdecimal을 리턴하지 않는한 자바의 math라이브러리를 사용하지 않기.
    • 자바의 부동소수점이나 더블 원시값이 rationalize된 값을 사용하지 않는다.
    • 고정밀 계산이 반드시 필요한 경우라면 rational을 써라.
    • Only convert to a floating-point representation as a last resort. (last resort?)
  • 추가 분모,분자 부분 추출하는 함수

(numerator (/ 123 10))
;=> 123
(denominator (/ 123 10))
;=> 10
  • 완벽한 정밀도를 필요로하지 않겠지만, 그렇게 해야할 때, 클로저는 온전함을 유지할 수 있는 툴을 제공한다. 하지만 관리하는 책임은 너한테 있다.

Caveats of rationality? (유리수의 적합성?)

  • 클로저 역시 다른 언어와 같이 변환하는 데 비용이 들기 때문에, 속도가 적합성보다 중요할 때에는 rational연산을 사용하는 것이 좋지 않다.
  • 숫자는 커버했고, 이제는 친숙하지 않은 두 개념을 보자 keyword와 symbol!

키워드를 사용하는 경우

  • 클로저는 평가하면 자기 자신이 나오는 타입니다.
:a-keyword
;;=> :a-keyword
::also-a-keyword
;;=> :user/also-a-keyword

키워드의 용도

키워드는 항상 자신을 가리킨다. 하지만 심볼은 그렇지 않다. 이건 :magma라는 키워드는 :magma라는 값을 가진다는 의미다. 반면 심볼 ruins는 다른 클로저의 값이나 레퍼런스를 가리킬지도 모른다.

키로 사용

클로저 코드에서 대부분 키워드는 맵의 키로 사용된다.

(def population {:zombies 2700, :humans 9})
(get population :zombies)
;=> 2700
(println (/ (get population :zombies)
            (get population :humans))
         "zombies per capita")
; 300 zombies per capita

get 함수로 맵에서 값을 찾을 수 있다. 하지만 키워드의 특별한 기능으로 코드를 좀더 직관적으로 작성할 수 있다.

함수로 사용

키워드는 함수로 사용될 수 있다. 함수로 사용되는 경우 맵에서 값을 찾는 기능을 한다. 그래서 키워드가 맵의 키로 많이 사용된다. 그리고 get을 사용하는 것 보다 코드를 더 직관적이다.

(:zombies population)
;=> 2700
(println (/ (:zombies population)
            (:humans population))
         "zombies per capita")
; 300 zombies per capita

열거형으로 사용

클로저 코드에서 종종 :small, :medium, :large과 같이 열거형으로 사용되기도 한다.

Mutimethod dispatch 값으로 사용

키워드가 열거형으로 종종 사용되기 때문에 multimethod dispatch 값으로 사용되기도 한다. 이는 secion 9.2에서 더 알아본다.

지시어로 사용

키워드의 또 다른 용도는 함수, 매크로, multimethod의 지시어이다. 예를 들어보면 두 숫자를 받아 두 숫자의 lazy 시퀀스로 된 range를 리턴하는 함수가 있다고 하자. 이 함수는 두번째 인자로 :toujours를 넘기면 첫번째 인자 부터 무한히 증가하는 시퀀스를 리턴한다.

(defn pour [lb ub]
  (cond
    (= ub :toujours) (iterate inc lb)
    :else (range lb ub)))
(pour 1 10)
 ;=> (1 2 3 4 5 6 7 8 9)
(pour 1 :toujours)
 ; ... runs forever

위의 예제에서 cond 매크로의 :else 키워드도 지시어로 사용되는 예이다.

키워드를 어딘가 속하게 하기 (qualifed keyowrd)

키워드는 어떤 네임스페이스에도 속하지 않는다. 두개의 콜론을 써서 사용하는 경우 앞에 네임스페이스가 나타나도 말이다.

::not-in-ns
;=> :user/not-in-ns

위 예제에서는 네임스페이스를 명시하지 않았기 때문에 repl의 기본 네임스페이스인 user가 앞에 붙는다.

(ns another)
another=> :user/in-another
;=> :user/in-another

위의 예제는 another 네임스페이스에서 user 네임스페이스를 앞에 붙여서 만들어 보았다. 어느 네임스페이스에서도 키워드의 prefix는 아무것이나 사용할 수 있다. 결국 키워드 prefix는 네임스페이스와는 관계없고 단지 컨텍스트의 의미를 명확하게 하기 위해서 사용한다.

도메인으로 부터 분리하기

아래와 같은 코드로 네임스페이스에 따라 키워드 지시어가 다르게 동작하는 코드를 작성할 수 있다.

(defn do-blowfish [directive]
  (case directive
    :aquarium/blowfish (println "feed the fish")
    :crypto/blowfish   (println "encode the message")
    :blowfish          (println "not sure what to do")))
(ns crypto)
(user/do-blowfish :blowfish)
; not sure what to do
(user/do-blowfish ::blowfish)
; encode the message
(ns aquarium)
(user/do-blowfish :blowfish)
; not sure what to do
(user/do-blowfish ::blowfish)
; feed the fish

심볼의 해석

클로저의 심볼은 다른 언어에서 아이디라고 불리우는 것들과 유사하다. 넛셀에서 심볼은 주어진 값에 이름으로 사용된다. 클로저에서는 심볼이 symbol, quote, ' 기호로 직접 참조할 수 있다. 심볼은 저마다 별개의 엔티티이다. 다음 예에서와 같이 심볼은 키워드와 다르게 같은 이름이라도 유일한 것이아니다.

(identical? 'goat 'goat)
;=> false

identical?에서 false를 리턴하는 것을 보면 두개의 심볼은 이름만 공유하는 다른 객체다. 하지만 이름은 동등 연산자가 동작하는 기준이 된다.

(= 'goat 'goat)
;=> true
(name 'goat)
"goat"

identical? 함수는 심볼이 같은 객체라면 true를 리턴한다.

(let [x 'goat y x]
          (identical? x y))
;=> true

그럼 여기서 질문이 생긴다. 왜 두개의 심볼을 다른 객체로 생성할까? 답은 메타데이터에 있다.

메타데이터

with-meta 함수는 객체와 맵을 파라미터로 받고 메타데이터가 포함된 같은 타입의 새로운 객체를 리턴한다.

(let [x (with-meta 'goat {:ornery true})
              y (with-meta 'goat {:ornery false})]
          [(= x y)
           (identical? x y)
           (meta x)
           (meta y)])
;=> [true false {:ornery true} {:ornery false}]

키워드는 메타데이터를 가지지 못한다.

심볼과 네임스페이스

키워드 처럼 심볼도 어느 네임스페이스에도 속하지 않는다.

(ns where-is)
(def a-symbol 'where-am-i)
a-symbol
;=> where-am-i
(resolve 'a-symbol)
;=> #'where-is/a-symbol
`a-symbol
;=> where-is/a-symbol

위의 예에서 첫번째 실행 결과는 예상대로 where-am-i 값을 가진다. 하지만 resolve와 신택스쿼트(```)를 사용한 결과는 네임스페이스가 붙어있는 심볼처럼 나온다. 이것은 심볼의 소속을 심볼이 가지고 있는 것이 아니고 실행 환경에 영향을 받기 때문이다. 이것은 클래스명과 함께 분류된 심볼에도 적용된다.(?) 이러한 특징은 매크로를 사용할때 좋은 점으로 작용하는데 8장에서 다룬다. 여기서는 간만보겠다.

Lisp-1

클로저는 이름을 해석하는데 있어 함수와 값을 바인딩하는 것을 똑같이 해석하는 Lisp-1의 형태를 따른다. Common Lisp과 같은 Lisp-2에서는 이름이 함수 호출 위치에 있는지 아규먼트 위치에 있는지에 따라 다르게 해석된다.

(defn best [f xs]
(reduce #(if (f % %2) % %2) xs))
(best > [1 3 4 2 7 5 3])
;=> 7
;; This is Common Lisp and NOT Clojure code
(defun best (f xs)
  (reduce #'(lambda (l r)
              (if (funcall f l r) l r))
            xs))
(best #'> '(1 3 4 2 7 5 3))
;=> 7

Lisp-2 (CommonLisp)과 같은 경우에 함수를 인자로 넘기기 위해서는 심볼의 형태로 넘겨야하지만 클로저에서는 그냥 넘기면 된다. 그냥 넘기면 코드가 보기좋다. 하지만 드물게 발생하는 Name Shadowing 때문에 버그를 발생 시킬 수 있다.

Regular expressions-the second problem (lester)

4.5.1 Syntax

A literal regular expression in Clojure looks like this:

#"an example pattern”
(class #"example")
;=> java.util.regex.Pattern
  • Java의 정규표현식에서는 "" 두개...
(java.util.regex.Pattern/compile "\\d")
;=> #"\d"
  • (?<flag>)
(re-seq #"(?i)yo" "asdYO")
;=> ("YO")

4.5.2 Regular-expression functions

  • re-seq function is Clojure’s regex workhorse. It returns a lazy seq of all matches in a string
(re-seq #"\w+" "one-two/three")
;=> ("one" "two" "three")

(re-seq #"\w*(\w)" "one-two/three")
;=> (["one" "e"] ["two" "o"] ["three" "e"])

4.5.3 Beware of mutable matchers

  • Matcher 주의!
  • non thread safe!