; 주석은 세미콜론(;)으로 시작합니다. ; Clojure는 "폼(forms)"으로 구성되었으며, ; 폼은 괄호로 감싸져있으며, 공백으로 구분된 것들이 나열된 것입니다. ; ; clojure의 reader는 첫번째로 오는 것을 ; 함수 혹은 매크로를 호출하는 것, 그리고 나머지를 인자라고 가정합니다. ; namespace를 지정하기 위해, 파일에서 우선적으로 호출해야될 것은 ns입니다. (ns learnclojure) ; 간단한 예제들: ; str 은 인자로 받은 것들을 하나의 문자열로 만들어줍니다. (str "Hello" " " "World") ; => "Hello World" ; 직관적인 수학 함수들을 갖고 있습니다. (+ 1 1) ; => 2 (- 2 1) ; => 1 (* 1 2) ; => 2 (/ 2 1) ; => 2 ; = 로 동일성을 판별할 수 있습니다. (= 1 1) ; => true (= 2 1) ; => false ; 논리연산을 위한 not 역시 필요합니다. (not true) ; => false ; 중첩된 폼(forms)은 기대한대로 동작합니다. (+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2 ; 타입 ;;;;;;;;;;;;; ; Clojure는 부울(boolean), 문자열, 숫자를 위해 Java의 object 타입을 이용합니다. ; `class` 를 이용하여 이를 확인할 수 있습니다. (class 1) ; 정수는 기본적으로 java.lang.Long입니다. (class 1.); 소수는 java.lang.Double입니다. (class ""); 문자열은 쌍따옴표로 감싸져 있으며, java.lang.String입니다. (class false) ; 부울값은 java.lang.Boolean입니다. (class nil); nil은 "null"값입니다. ; 데이터 리스트 자체를 만들고자 한다면, ; '를 이용하여 평가(evaluate)되지 않도록 막아야 합니다. '(+ 1 2) ; => (+ 1 2) ; (quote (+ 1 2)) 를 줄여서 쓴것 ; quote 가 된 리스트를 평가할 수 도 있습니다. (eval '(+ 1 2)) ; => 3 ; 컬렉션(Collections) & 시퀀스(Sequences) ;;;;;;;;;;;;;;;;;;; ; 리스트(List)는 연결된(linked-list) 자료구조이며, 벡터(Vector)는 배열이 뒤로붙는(array-backed) 자료구조입니다. ; 리스트와 벡터 모두 java 클래스입니다! (class [1 2 3]); => clojure.lang.PersistentVector (class '(1 2 3)); => clojure.lang.PersistentList ; 간단하게 (1 2 3)로 리스트를 나타낼 수 있지만, ; reader가 함수라고 여기지 못하게 quote(')를 해줘야 합니다. ; 따라서, (list 1 2 3)는 '(1 2 3)와 같습니다. ; "컬렉션"은 단순하게 데이터의 그룹입니다. ; 리스트와 벡터 모두 컬렉션입니다: (coll? '(1 2 3)) ; => true (coll? [1 2 3]) ; => true ; "시퀀스" (seq) 는 데이터 리스트를 추상적으로 기술한 것입니다. ; 리스트는 시퀀스입니다. (seq? '(1 2 3)) ; => true (seq? [1 2 3]) ; => false ; 시퀀스는 접근하고자 하는 항목만 제공해주면 됩니다. ; 따라서, 시퀀스는 lazy 할 수 있습니다 -- 무한하게 늘어나는 것을 정의할 수 있습니다: (range 4) ; => (0 1 2 3) (range) ; => (0 1 2 3 4 ...) (an infinite series) (take 4 (range)) ; (0 1 2 3) ; cons 를 이용하여 리스트나 벡터의 시작부에 항목을 추가할 수 있습니다. (cons 4 [1 2 3]) ; => (4 1 2 3) (cons 4 '(1 2 3)) ; => (4 1 2 3) ; conj 는 컬렉션에 가장 효율적인 방식으로 항목을 추가합니다. ; 리스트는 시작부분에 삽입하고, 벡터는 끝부분에 삽입합니다. (conj [1 2 3] 4) ; => [1 2 3 4] (conj '(1 2 3) 4) ; => (4 1 2 3) ; concat 을 이용하여 리스트와 벡터를 서로 합칠 수 있습니다. (concat [1 2] '(3 4)) ; => (1 2 3 4) ; filter, map 을 이용하여 컬렉션을 다룰 수 있습니다. (map inc [1 2 3]) ; => (2 3 4) (filter even? [1 2 3]) ; => (2) ; reduce 를 이용하여 줄여나갈 수 있습니다. (reduce + [1 2 3 4]) ; = (+ (+ (+ 1 2) 3) 4) ; => 10 ; reduce 는 초기 값을 인자로 취할 수 도 있습니다. (reduce conj [] '(3 2 1)) ; = (conj (conj (conj [] 3) 2) 1) ; => [3 2 1] ; 함수 ;;;;;;;;;;;;;;;;;;;;; ; fn 을 이용하여 함수를 만들 수 있습니다 . ; 함수는 항상 마지막 문장을 반환합니다. (fn [] "Hello World") ; => fn ; (정의한 것을 호출하기 위해선, 괄호가 더 필요합니다.) ((fn [] "Hello World")) ; => "Hello World" ; def 를 이용하여 var 를 만들 수 있습니다. (def x 1) x ; => 1 ; var 에 함수를 할당시켜보겠습니다. (def hello-world (fn [] "Hello World")) (hello-world) ; => "Hello World" ; defn 을 이용하여 짧게 쓸 수 도 있습니다. (defn hello-world [] "Hello World") ; [] 는 함수의 인자 목록을 나타냅니다. (defn hello [name] (str "Hello " name)) (hello "Steve") ; => "Hello Steve" ; 약자(shorthand)를 써서 함수를 만들 수 도 있습니다: (def hello2 #(str "Hello " %1)) (hello2 "Fanny") ; => "Hello Fanny" ; 함수가 다양한 인자를 받도록 정의할 수 도 있습니다. (defn hello3 ([] "Hello World") ([name] (str "Hello " name))) (hello3 "Jake") ; => "Hello Jake" (hello3) ; => "Hello World" ; 함수는 여러 인자를 시퀀스로 취할 수 있습니다. (defn count-args [& args] (str "You passed " (count args) " args: " args)) (count-args 1 2 3) ; => "You passed 3 args: (1 2 3)" ; 개별적으로 받는 것과, 시퀀스로 취하는 것을 같이 쓸 수 도 있습니다. (defn hello-count [name & args] (str "Hello " name ", you passed " (count args) " extra args")) (hello-count "Finn" 1 2 3) ; => "Hello Finn, you passed 3 extra args" ; 맵(Maps) ;;;;;;;;;; ; 해쉬맵(hash map)과 배열맵(array map)은 공통된 인터페이스를 공유합니다. ; 해쉬맵은 찾기가 빠르지만, 키의 순서가 유지되지 않습니다. (class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap (class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap ; 배열맵은 여러 연산을 거쳐 자연스레 해쉬맵이 됩니다. ; 만일 이게 커진다 하더라도, 걱정할 필요가 없습니다. ; 맵은 해쉬가 가능한 타입이라면 어떠한 것이든 키로써 활용이 가능하지만, 보통 키워드를 이용하는 것이 가장 좋습니다. ; 키워드(Keyword)는 문자열과 비슷하지만, 보다 효율적인 면이 있습니다. (class :a) ; => clojure.lang.Keyword (def stringmap {"a" 1, "b" 2, "c" 3}) stringmap ; => {"a" 1, "b" 2, "c" 3} (def keymap {:a 1, :b 2, :c 3}) keymap ; => {:a 1, :c 3, :b 2} ; 여기서, 쉽표가 공백으로 취급되며, 아무 일도 하지 않는다는 것을 주목하시기 바랍니다. ; 맵에서 값을 얻어오기 위해선, 함수로써 맵을 호출해야 합니다. (stringmap "a") ; => 1 (keymap :a) ; => 1 ; 키워드 역시 맵에서 함수를 얻어올 때 사용할 수 있습니다! (:b keymap) ; => 2 ; 하지만, 문자열로는 하면 안됩니다. ;("a" stringmap) ; => Exception: java.lang.String cannot be cast to clojure.lang.IFn ; 없는 값을 얻어오고자 하면, nil이 반환됩니다. (stringmap "d") ; => nil ; assoc 를 이용하여 해쉬맵에 새로운 키를 추가할 수 있습니다. (def newkeymap (assoc keymap :d 4)) newkeymap ; => {:a 1, :b 2, :c 3, :d 4} ; 하지만, 변경할 수 없는(immutable) clojure 타입이라는 것을 기억해야 합니다! keymap ; => {:a 1, :b 2, :c 3} ; dissoc 를 이용하여 키를 제거할 수 있습니다. (dissoc keymap :a :b) ; => {:c 3} ; 쎗(Set:집합) ;;;;;; (class #{1 2 3}) ; => clojure.lang.PersistentHashSet (set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3} ; conj 로 항목을 추가할 수 있습니다. (conj #{1 2 3} 4) ; => #{1 2 3 4} ; disj 로 제거할 수 도 있습니다. (disj #{1 2 3} 1) ; => #{2 3} ; 존재하는지 확인할 목적으로, 쎗을 함수로 사용할 수 도 있습니다. (#{1 2 3} 1) ; => 1 (#{1 2 3} 4) ; => nil ; clojure.sets 네임스페이스(namespace)에는 더 많은 함수들이 있습니다. ; 유용한 폼(forms) ;;;;;;;;;;;;;;;;; ; clojure에선, if 와 매크로(macro)를 가지고, ; 다른 여러 논리 연산들을 만들 수 있습니다. (if false "a" "b") ; => "b" (if false "a") ; => nil ; let 을 이용하여 임시적으로 바인딩(binding)을 구축할 수 있습니다. (let [a 1 b 2] (> a b)) ; => false ; do 로 문단을 묶을 수 도 있습니다. (do (print "Hello") "World") ; => "World" (prints "Hello") ; 함수는 암시적으로 do 를 가지고 있습니다. (defn print-and-say-hello [name] (print "Saying hello to " name) (str "Hello " name)) (print-and-say-hello "Jeff") ;=> "Hello Jeff" (prints "Saying hello to Jeff") ; let 역시 그러합니다. (let [name "Urkel"] (print "Saying hello to " name) (str "Hello " name)) ; => "Hello Urkel" (prints "Saying hello to Urkel") ; 모듈(Modules) ;;;;;;;;;;;;;;; ; "use" 를 이용하여 module에 있는 모든 함수들을 얻어올 수 있습니다. (use 'clojure.set) ; 이제 쎗(set:집합)연산을 사용 할 수 있습니다. (intersection #{1 2 3} #{2 3 4}) ; => #{2 3} (difference #{1 2 3} #{2 3 4}) ; => #{1} ; 함수들 중에 일 부분만을 가져올 수 도 있습니다. (use '[clojure.set :only [intersection]]) ; require 를 이용하여 모듈을 import할 수 있습니다. (require 'clojure.string) ; / 를 이용하여 모듈에 있는 함수를 호출 할 수 있습니다. ; 여기, clojure.string 라는 모듈에, blank? 라는 함수가 있습니다. (clojure.string/blank? "") ; => true ; import시, 모듈에 짧은 이름을 붙여줄 수 있습니다. (require '[clojure.string :as str]) (str/replace "This is a test." #"[a-o]" str/upper-case) ; => "THIs Is A tEst." ; (#"" denotes a regular expression literal) ; :require 를 이용하여, 네임스페이스에서 require 를 사용할 수 있습니다. ; 아레와 같은 방법을 이용하면, 모듈을 quote하지 않아도 됩니다. (ns test (:require [clojure.string :as str] [clojure.set :as set])) ; Java ;;;;;;;;;;;;;;;;; ; Java는 유용한 많은 표준 라이브러리를 가지고 있으며, ; 이를 어떻게 활용할 수 있는지 알아보도록 하겠습니다. ; import 로 java 모듈을 불러올 수 있습니다. (import java.util.Date) ; ns 와 함께 import 를 할 수 도 있습니다. (ns test (:import java.util.Date java.util.Calendar)) ; 새로운 인스턴스를 만들기 위해선, 클래스 이름 끝에 "."을 찍습니다. (Date.) ; ; . 을 이용하여 메소드를 호출할 수 있습니다. ; 아니면, 줄여서 ".메소드"로도 호출 할 수 있습니다. (. (Date.) getTime) ; (.getTime (Date.)) ; exactly the same thing. ; / 를 이용하여 정적메소드를 호출 할 수 있습니다. (System/currentTimeMillis) ; (system is always present) ; doto 를 이용하여 상태가 변하는(mutable) 클래스들을 좀 더 편하게(tolerable) 다룰 수 있습니다. (import java.util.Calendar) (doto (Calendar/getInstance) (.set 2000 1 1 0 0 0) .getTime) ; => A Date. set to 2000-01-01 00:00:00 ; STM ;;;;;;;;;;;;;;;;; ; Software Transactional Memory 는 clojure가 영구적인(persistent) 상태를 다루는 방식입니다. ; clojure가 이용하는 몇몇 자료형(construct)이 있습니다. ; 가장 단순한 것은 atom 입니다. 초기 값을 넣어보도록 하겠습니다. (def my-atom (atom {})) ; swap! 으로 atom을 갱신(update)할 수 있습니다! ; swap! 은 함수를 인자로 받아, 그 함수에 대해 현재 atom에 들어있는 값을 첫번째 인자로, ; 나머지를 두번째 인자로 하여 호출합니다. (swap! my-atom assoc :a 1) ; Sets my-atom to the result of (assoc {} :a 1) (swap! my-atom assoc :b 2) ; Sets my-atom to the result of (assoc {:a 1} :b 2) ; '@' 를 이용하여 atom을 역참조(dereference)하여 값을 얻을 수 있습니다. my-atom ;=> Atom<#...> (atom 객체가 반환됩니다.) @my-atom ; => {:a 1 :b 2} ; 여기 atom을 이용한 단순한 카운터가 있습니다. (def counter (atom 0)) (defn inc-counter [] (swap! counter inc)) (inc-counter) (inc-counter) (inc-counter) (inc-counter) (inc-counter) @counter ; => 5 ; STM을 구성하는 다른 것들에는 ref 와 agent 가 있습니다. ; Refs: http://clojure.org/refs ; Agents: http://clojure.org/agents