Skip to content

2. Drinking from the Clojure firehose

Minsun Lee edited this page Jul 28, 2014 · 30 revisions

해석하기 애매한 것들

  • evaluate : 평가 또는 계산이라고 했는데, 뭔가 영어 이상으로 와닿지 않는!
  • literal syntax : 문법 구문이라고 했는데, 뭔가 애매한 말 중 하나인듯
  • store : 저장이라고 했는데, 그냥 가진다라고 하는게 좋을 거 같기도..
  • arbitrarily precise

이 과에서는 다음 내용을 배웁니다.

  • Scalars: the base data types
  • Putting things together: collections
  • Making things happen: functions
  • Vars are not variables
  • Locals, loops, and blocks
  • Preventing things from happening: quoting
  • Using host libraries via interop
  • Exceptional circumstances
  • Modularizing code with namespaces

  • 필수적인 기초들
  • 튜토리얼을 바로 제공하는 것이 이상해 보이겠지만,언어의 생각을 소개하는데 가장 중요
  • 클로져로 프로그래밍 해봤다면 리뷰가 될것이고 아니라면
  • 클로저코드 쓰기 시작하는데 필요한 모든 것을 알려줄것이다.
  • 이 챕터의 대부분의 경우 예제가 나오는데 이는 즉시 강조(이해를위한)하기 위한 형식적인 것이다.
  • 모드 피쳐(기능)에 매달릴 필요는 없다. 너는 결국 알게 될 것!
  • 클로저 실행은 REPL(Read-Eval-Print Loop)로 할 것이다.
  • REPL 세션을 실행하면, 다음과 같은 간단한 프롬프트가 나온다 :
user =>
  • user 프롬프트가 REPL의 가장 상위 네임스페이다.
  • 이 상태에서 클로저는 표현식을 기다린다.
  • 유효한 클로저 표현식은 다음으로 구성된다. ( numbers, symbols, keywords, Booleans, characters, functions, function calls, macros, strings, literal maps, vectors, queues, records, sets)
  • numbers, strings, keywords를 포함한 몇몇 표현식은 enter를 할 때 스스로 계산이 된다.
  • 주석은 ;(세미콜론) 뒤에 쓴다.

Scalars: the base data types

  • 스칼라 데이터 타입
  • 클로저는 풍부한 데이터 타입 set을 가짐
  • 다른 대부분의 프로그래밍 언어와 마찬가지로, 각각 하나의 데이터 데이터 단위를 표현하는 integer,string,floating-point와 같은 스칼라타입을 제공.
  • 서로 다른 카테고리의 스칼라 타입을 제공한다. (integers/floats/rationals/symbols/keywords/strings/characters/Booleans/regex patterns)
  • 예제와 함께 차례대로 배워봅시다~

Numbers

Number는 다음으로 구성

  • 0 - 9 까지 숫자

  • 소수점

  • +, - 기호

  • e- 지수표현

  • 이러한 요소 이외에도, 클로저에서 다루는 숫자는 8진수나 16진수를 취할 수 있으며, 또한 임의의 소수자리(M flag)나 임의의 크기의 정수(N flag)를 나타내는 옵션을 포함한다.

  • 많은 프로그래밍 언어에서 숫자의 정밀도 (소수점 이하)는 host platform, 혹은 자바와 C#의 경우처럼 언어 사양에 의해 제한된다. 클로저는 임의의 정밀도 숫자를 제공하지만, default로 호스트 언어 (Java 혹은 Javascript)의 기본형식을 따른다. JVM의 경우도 안정성을 위해 오버플로우 발생시 예외를 발생시킨다.

Integers

  • 모든 숫자 집합, +,-
  • 클로저에서, (상황에 따라 다른 특정한 타입으로도 지정하는 경우도 있지만) 일단 부호나 숫자로 시작되어 숫자로만 이어지고 있다면 임의의 정수 형태로 저장된다.
  • 클로저에서 integer는 이론적으로는 무한대 값을 가질 수 있지만, 실제로는 메모리에서 가능한 크기만큼 가짐
  • 다음의 숫자들은 클로저에서 integer로 받아들여짐
49
+9
-107
991778647261948849222819828311491035886734385827028118707676848307166514
  • 마지막 숫자 빼고는 모두 Java의 long처럼 읽혀진다.
  • 마지막 숫자(길게 나열된 숫자들)는 너무 커서 BigInt로 읽혀진다.
  • 다음의 숫자들은 같은 숫자에 대해, 각각 10진법, 16진법, 8진법 radix-32, 이진표기로 나타낸 것이다.
[127 0x7F 0177 32r3V 2r01111111]
        ;=> [127 127 127 127 127]
  • base36
  • (!todo)
  • 최대 표현가능한 진법은 36개( 0~9까지 + 26개의 ASCII 문자수)

Floating-point numbers

  • 부동소숫점 숫자는 유리수의 소수전개(!todo:decimal point)
  • 클로저의 integer구현처럼, 임의의 숫자 (!todo: arbitrarily precise)
  • 부동소숫점 숫자는 전통적인 형식인 몇개의 숫자와, 소숫점, 그리고 몇개의 숫자로 이루어짐
  • 하지만, 대문자나 소문자 E로 구분된 지수부분에 따라오는 유효부분이 있는 곳에 지수형식(과학적 기수법)도 취할 수 있다.
  • 부동 소숫점 숫자의 예
1.17
+1.22
-2.
366e7
32e-14
10.7e-3

숫자는 다른 프로그래밍 언어와 크게 비슷하다. 이제는 리습과 리습의 영향을 받은 언어들의 뚜렷한 특징을 갖는 스칼라타입을 살피자!

Rationals

  • 유리수
  • 클로저는 정수와 부동소수점 뿐만 아니라 Rational 타입을 제공한다.
  • Rational타입은 부동소숫점 보다 표현하기에 좀 더 세밀하고 정확하다.
  • Rational은 고전적으로 정수의 분자와 분모로 표현되고, 그게 클로저에서 정확하게 표현하는 방법이다.
  • Raitional type 예
22/7
-7/22
1028798300297636767687409028872/88829897008789478784
-103/4

클로저에서 rational 숫자로 알아둘 것은 그들은 가능한한 단순화한다는 것이다. — rational 100/4는 integer 25로 해석한다.

Symbols

  • 클로저에서 Symbol은 스스로 그 자체인 객체이지만, 가끔은 다른 값을 표현하기도 함
(def yucky-pi 22/7)
yucky-pi
;;=> 22/7
  • 숫자나 문자열(string)이 계산될 때에는 정확히 같은 object를 반환받는다. 반면 symbol의 경우, 그 symbol이 현재의 문맥에서 참조하는 어떠한 값이든 반환받는다. 즉, symbol은 대개 함수 인자, 지역변수, 전역변수나 자바 클래스를 참조한다.

Keywords

  • keywords는 항상 그들 스스로로 판단(evaluate)을 내리는 것 외에는 심볼과 비슷하다.
  • 클로저에서는 심볼보다 키워드의 사용을 훨씬 더 많이 볼 것이다.
  • keyword 문법구문 형식은 다음과 같다
:chumby
:2
:?
:ThisIsTheNameOfaKeyword

  • 키워드 앞에 :(콜론)이 붙지만, 이것은 문법구문의 일부이지, 이름 자체의 일부는 아니다.

  • 자세한 것은 4.3에서 살필 것

  • (!todo literal syntax)

Strings

  • 클로저의 string은 다른 언어의 string과 비슷하게 표현된다.
  • string은 쌍따옴표로 둘러싸인 연속된 문자들이고, 개행 포함이 가능하다.
"This is a string"
"This is also a
			String"
  • 둘은 써진데로 저장된다. 하지만 REPL에서 출력할 때는, 복수 행의 스트링은 escape문자를 포함한다.
"This is also a\n String"

Characters

  • 클로저 Character는 \(백 슬래시)가 앞에 적힌 문법구문으로 쓰여지고, 저장은 자바의 Character 객체로 저장된다.
\a       ; The character lowercase a
\A       ; The character uppercase A
\u0042   ; The Unicode character uppercase B
\\       ; The back-slash character \
\u30DE   ; The Unicode katakana character ?

여기까지가 클로저의 스칼라 데이터 타입임돠 다음 섹션에서는 클로저의 정말 재밌는 콜렉션 데이터 타입에 대해 논의해봅시다.

Putting things together: collections

collection에 대해선 chap5에서 더욱 자세하게 다룰 것이다. 하지만 클로저 프로그램은 다양한 종류의 문자(literal) 콜렉션들로 구성되어 있기 때문에, list,vectors,maps,sets에 대해서 최소한이라도 알아두는 것이 도움이 될 것이다.

List

  • List는 Lisp(리습의 이름은 결국 _list processing_으로 부터 왔다.)에서 고전적인 콜렉션 타입이고, 클로저도 예외는 아니다.
  • 문자(literal) 리스트는 괄호와 쓰인다.
(yankee hotel foxtrot)
  • list가 evaluate(평가)될 때, 첫번째 요소(여기서 yankee)는 함수, 매크로 또는 특별한 연산자로 결정된다.
  • yankee가 함수라면,리스트에 남은 다른 요소들은 순서대로 계산(evaluate)되고, 결과는 그것의 인수로 yankee에 전달된다.

** NOTE **

형식(form)이라는 것은 리스트, 벡터, 맵, 숫자, 키워드 혹은 심볼같은 것들처럼
평가되어야 하는어떠한 형태의 클로저 객체로 간주된다는 것에 주의해야 한다.
특별한 형식(special form)은 특수 구문이나 기본 클로저 양식을 통해 구현되어
있지 않는 특수한 평가 규칙이 수반되는 형식이다.
이 special form의 한 예로 자바에서의
상호연산을 목적으로 사용되는 .(dot) 연산자를 들 수 있다.
  • yankee가 매크로나 특별한 연산자라면, 리스트에 남은 다른 요소들은 반드시 계산(evaluate)되지는 않지만, 매크로나 연산자의 정의에 따라 처리된다.
  • List는 다른 콜렉션은 물론이고 어떤 타입도 요소로 가질 수 있다.
  • 다음 예들을 보자
(1 2 3 4)
()
(:fred ethel)
(1 2 (a b c) 4 5)
  • 참고로 일부 lisp과 달리,()로 쓰인,클로저에서 비어있는 list는 nil이 아니다.

Vectors

  • list와 같이 vector는 나열된 값(value)들을 저장한다.
  • 여러 차이점은 5.4에서 설명할 예정이지만, 중요하게 다른 두가지는 살펴보자.
  • 첫째, vector는 각진 괄호를 쓰는 문법구문 갖는다.
[1 2 :a :b :c]
  • 다른 중요한 차이점은 vector는 각 요소를 순서대로 계산(evaluate)하는 것이다.
  • 어떤 함수나 매크로 호출도 벡터 자체에서 수행되지 않는다. 그러나 만약 특정 리스트가 벡터 내부에 있을 때, 그 리스트는 리스트를 다룰 때 사용되는 일반적인 규칙을 따른다. 리스트와 마찬가지로 벡터는 heterogeneous한 타입이다. 그렇기 때문에 비어 있는 벡터 []는 nil처럼 사용되지 않는다.

Maps

  • Map은 유일한 key들과 key마다 하나씩 있는 value을 가진다.
    • 몇몇 언어와 라이브러리에 있는 dictioanarieshash 와 비슷하다
  • 클로저는 서로 다른 속성을 가진 Map의 여러 타입을 갖는다.
    • 지금 모른다고 걱정할 필요는 없다.
  • Map은 중괄호{}사이에 key와 value가 반복되는 구문문법으로 쓰여진다.
  • 콤마 (,)는 자주 쌍(key-value) 사이에 쓰이지만, 클로저의 모든 문맥에서와같이 공백처럼 간주된다. (공백으로 해도 무)
{1 "one", 2 "two", 3 "three"}
  • vector와 마찬가지로 map 구문의 모든 요소(각 key와 각 value)는 결과가 map안에서 저장되기 이전에 평가(evaluate) 된다.
  • vector와 다르게, 계산(evaluate)순서는 보장되지 않는다.
  • Map은 key나 value에 대해 어떤 타입이나 요소로 가질 수 있고, 비어있는 map{}은 nil이 아니다.

Sets

  • Sets은 0개또는 그 이상의 유일한 요소를 갖는다.
  • 해시(#)로 시작하는 중괄호와 쓰인다.
#{1 2 "three" :four 0x5}
  • 비어있는 Set #{}는 nil이 아니다.

여기까지가 기본적인 collection타입의 전부다. Chap4에서 각 타입의 장단점과 queue를 다룰 것이다.

Making things happen: Calling functions

  • 클로저에서 함수는 first-class 타입이다. 뭔말이냐면 임의의 값(value)과 동일하게 사용될 수 있다.
  • 함수는 list나 다른 collection로 간주된 vars로 저장될 수 있고, 인수로 전달될 수도 있으며, 다른 함수의 리턴 결과도 될 수도 있다.
(vector 1 2 3)         ;;1,2,3을 vector라는 함수에 전달
;;=> [1 2 3]		   ;;vertor를 반환
  • C-style에서 사용되는 prefix notation 이 infix notation에 비해 갖는 가장 큰 이점은 prefix notation은 연산자당 피연산자의 개수를 임의로 취할 수 있으나, infix notation은 두 개로 제한된다는 것에 있다. 또 다른 이점으로는 prefix notataion은 연산자의 우선순위 문제를 없애준다는 점인데, 이게 structuring code에 비해 크게 좋다고 하기는 애매하다. 클로저는 연산자 표기법과 일반적인 함수 호출을 구분하지 못한다. 즉, 모든 클로저 생성자, 함수, 매크로, 연산자는 prefix에 의해 형성되거나 완전히 괄호처리된 형태로 표기된다. 이러한 통일된 구조는 Lisp와 같은 언어가 제공하는 놀라운 유연성을 위한 기초를 형성한다

  • infix notation에 비해 prefix notation의 직접적인 명백한 이점은 C스타일의 언어는 former가 허락한다 연산자마다 피연산자 숫자가 infix는 두개의 피연산자만 허락하는데 비해

  • Prefix notation으로 코드를 구성하는데 작은 장점은 연산자가 앞서는 문제를 제거한다는 것

  • 클로저는 연산자 notation과 함수 호출을 구분을 못한다.

  • 모든 클로저 생성자, 함수, 매크로, 연산자는 prefix 또는 완전히 괄호로 싸여진 표기를 쓴다.
  • 이 균일한 구조는 리습같은 언어가 제공하는 놀라운 유연함에 기초한다.

Vars are not variables

  • 프로그래머들은 대부분 변수(variables)와 변형(mutable)에 익숙하다.
  • 클로저에서 variable과 가장 유사한 것은 var다.
  • var는 symbol에 의해 명명되었고 단일 값(value)를 갖는다.
    • 이 값은 프로그램 실행중에 변경 가능하다.
    • but this is best reserved for the programmer making manual changes
    • 또한 var의 값은 함수인자와 지역변수에 의해 shadowed(지역변수로 배정)될 수 있다. (비록 shadowing이 원래의 값을 바꾸진 않는다)
  • 클로저에서 var를 생성하는 가장 보통의 방법은 def를 이용하는 것이다.
(def x 42)
  • def를 이용해 42라는 값을 심볼 x에 지정하면, 이 때 root binding이라고 알려진 기능을 생성한다.
  • 흔한 케이스는 symbol x가 42에 묶이는 것이지만 var는 value를 요구하지 않는다.
(def y)
y
;=> java.lang.IllegalStateException: Var user/y is unbound.

Functions

Locals, loops, and blocks

Preventing things from happening: quoting

Using host libraries via interop

Clojure는 자바를 기반으로 하여 자바의 클래스들을 사용할 수 있다.

Static 클래스 맴버에 접근하기

java.util.Locale/JAPAN
;=> #<Locale ja_JP>
(Math/sqrt 9)
;=> 3.0
  • java.lang에 있는 클래스들은 바로 쓸 수 있다.
  • ClojureScript static 맴버에 접근하는 기능을 제공하지 않는다. (자바스크립트에서 제공하지 않기 때문에)

인스턴스 생성하기

  • new라는 연산자로 자바 인스턴스를 생성할 수 있다.
(new java.awt.Point 0 1)
;=> #<Point java.awt.Point[x=0,y=1]>)
  • 위는 java.awt.Point 생성자에 0, 1을 파라미터로 해서 인스턴스를 생성하는 예이다.
(new java.util.HashMap {"foo" 42 "bar" 9 "baz" "quux"})
;=> {"baz" "quux", "foo" 42, "bar" 9}
  • Collection을 생성자의 파라미터로 사용하는 경우 Clojure의 Collection과 호환이 가능하다.
(java.util.HashMap. {"foo" 42 "bar" 9 "baz" "quux"})
;=> {"baz" "quux", "foo" 42, "bar" 9}
  • 인스턴스를 생성하는 두번째 방법으로 new 대신 클래스이름 뒤에 .을 붙여 생성할 수 있다.

  • 클로저 스크립트에서도 이와 같이 사용할 수 있다.

(js/Date.)
;=> #inst "2013-02-01T15:10:44.727-00:00"

. 연산자를 사용해서 인스턴스 맴버에 접근하기

  • .과 -를 가지고 인스턴스 필드에 접근할 수 있다.
(.-x (java.awt.Point. 10 20))
;=> 10
  • .과 메서드명을 가지고 매서드를 호출 할 수 있다.
(.divide (java.math.BigDecimal. "42") 2M)
;=> 21M

필드에 값을 세팅하기

  • set! 함수로 필드에 값을 세팅할 수 있다.
(let [origin (java.awt.Point. 0 0)]
          (set! (.-x origin) 15)
          (str origin))
;=> "java.awt.Point[x=15,y=0]"

.. 매크로

  • 메서드의 호출이 연속되는 경우 다음과 같이 사용할 수 있다.
new java.util.Date().toString().endsWith("2014") /* Java code */

(.endsWith (.toString (java.util.Date.)) "2014") ; Clojure code
;=> true
  • 가독성을 위해 .. 매크로를 사용하면 아래와 같다.
(.. (java.util.Date.) toString (endsWith "2014"))
;=> true
  • ->, ->> 매크로는 ..과 유사하지만 일반적인 클로서 함수의 연속된 호출을 더 읽기 쉽게 해준다. (8장에서 다룬다.)

doto 매크로

  • doto 매크로를 사용하면 하나의 인스턴스에 연속된 메서드 호출을 보기 쉽게 만들 수 있다.
// This is Java, not Clojure
java.util.HashMap props = new java.util.HashMap();
props.put("HOME", "/home/me");        /* More java code. Sorry. */
props.put("SRC",  "src");
props.put("BIN",  "classes");
  • 아래와 같이 doto를 사용할 수 있다.
(doto (java.util.HashMap.)
          (.put "HOME" "/home/me")
          (.put "SRC"  "src")
          (.put "BIN"  "classes"))
        ;=> {"HOME" "/home/me", "BIN" "classes", "SRC" "src"}

클래스 정의

  • 클로저에서는 reify, deftype로 자바 인터페이스의 구현을 가능하게 해준다. (9장에서 다룸)
  • proxy라는 매크로는 인터페이스의 구현이나 상속을 받을 수 있게 해준다. (12장에서 다룸)
  • gen-class 매크로는 정적으로 이름을 가진 클래스를 생성할 수 있게 해준다. (12장에서 다룸)

Exceptional circumstances

예외 처리에는 throw, try, catch와 같은 것을 사용한다.

예외를 발생시키고 처리하기

  • 예외를 발생시키는 예제
(throw (Exception. "I done throwed"))
;=> java.lang.Exception: I done throwed ...
  • 예외를 처리하는 예제
(defn throw-catch [f]
  [(try
    (f)
    (catch ArithmeticException e "No dividing by zero!")
    (catch Exception e (str "You are so bad " (.getMessage e)))
    (finally (println "returning... ")))])

(throw-catch #(/ 10 5))
; returning...
;=> [2]

(throw-catch #(/ 10 0))
; returning...
;=> ["No dividing by zero!"]

(throw-catch #(throw (Exception. "Crybaby")))
; returning...
;=> ["You are so bad Crybaby"]
  • ClojureScript에서는 예외를 잡을 때 js를 써줘야 한다.
(try
  (throw (Error. "I done throwed in CLJS"))
  (catch js/Error err "I done catched in CLJS"))
;=> "I done catched in CLJS"

Modularizing code with namespaces

Clojure의 네임스페이스는 함수, 매크로, 값들을 묶을 수 있는 기능을 제공한다.

ns를 이용해서 네임스페이스 만들기

  • 새로운 네임스페이스를 만들기 위해서 ns 매크로를 사용한다.
(ns joy.ch2)
  • REPL 프롬프트는 아래와 같이 변경되고 현재 네임스페이스로 설정된다.
joy.ch2=>
  • 네임스페이스에서 생성한 것들은 그 네임스페이스에 속하게 된다.
  • ns로 현제 네임스페이스를 확인 할 수 있다.
joy.ch2=> (defn hello []
  (println "Hello Cleveland!"))

joy.ch2=> (defn report-ns []
  (str "The current namespace is " *ns*))

joy.ch2=> (report-ns)
;=> "The current namespace is joy.ch2"

joy.ch2=> hello
;=> #<ch2$hello joy.ch2$hello@2af8f5>
  • 네임스페이스를 변경하면 다른 네임스페이스에 있는 것들을 바로 사용하지 못하고 앞에 네임스페이스를 명시해줘야한다.
(ns joy.another)

joy.another=>

joy.another=> (report-ns)
; java.lang.
;   Unable to resolve symbol: report-ns in this context

joy.another=> (joy.ch2/report-ns)
;=> "The current namespace is joy.another"

:require를 이용해서 네임스페이스 불러오기

  • 네임스페이스에서 다른 네임스페이스 사용하기위해 require를 통해 로드 할 수 있다.
(ns joy.req
  (:require clojure.set))
(clojure.set/intersection #{1 2 3} #{3 4 5})
;;=> #{3}
  • :as를 사용하면 네임스페이스를 원하는 것으로 바꿔 사용할 수 있다.
(ns joy.req-alias
  (:require [clojure.set :as s]))
(s/intersection #{1 2 3} #{3 4 5})
;=> #{3}
  • 네임스페이스는 static 메서드 호출과 비슷해 보이는데 네임스페이스는 독립적으로 사용될 수 없고 static 메서드 호출은 독립적으로 사용될 수 있다.
clojure.set
; java.lang.ClassNotFoundException: clojure.set

java.lang.Object
;=> java.lang.Object
  • 혼란스럽지 않게 알아서 잘 쓰자

:refer를 사용해서 매핑을 만들고 로딩하기

  • 다른 네임스페이스에 있는 함수를 호출할 때 현재의 네임스페이스에 있는 함수처럼 매핑해서 쓰면 편리한데 이를 위해 :require의 :refer 옵션을 제공한다.
(ns joy.use-ex
  (:require [clojure.string :refer (capitalize)]))
(map capitalize ["kilgore" "trout"])
;=> ("Kilgore" "Trout")
  • :refer를 할때 추천하지 않지만 :all 값으로 모든 함수를 현재 네임스페이스 처럼 사용하게 할수 있다. (예전에는 :use를 사용했다.)

:refer를 사용해서 매핑 만들기

  • :require의 옵션이 아닌 독립적으로 :refer를 사용해서 로딩할 수 있는데 그러면 모든 함수가 현재 네임스페이스로 매핑이된다.
(ns joy.yet-another
  (:refer joy.ch2))
(report-ns)
;=> "The current namespace is joy.yet-another"
  • :rename 옵션으로 원래 함수명을 변경해서 로딩할 수도 있다. (:require에서도 사용가능)
(ns joy.yet-another
  (:refer clojure.set :rename {union onion}))
(onion #{1 2} #{4 5})
;=> #{1 2 4 5}

:import로 자바 클래스 로딩하기

  • :import를 사용하면 패키지명을 다 사용하지 않고 바로 자바 클래스를 사용할 수 있다.
(ns joy.java
          (:import [java.util HashMap]
                   [java.util.concurrent.atomic AtomicLong]))
(HashMap. {"happy?" true})
;=> {"happy?" true}
(AtomicLong. 42)
;=> 42
  • java.lang은 그냥 사용할 수 있다.
Clone this wiki locally