페이지 공유

Y분에 X를 배우다

여기서 X=Clojure

Clojure는 Java 가상머신을 위해 개발된 Lisp 계통의 언어입니다 이는 Common Lisp보다 순수 함수형 프로그래밍을 더욱 강조했으며, 상태를 있는 그대로 다루기 위해 다양한 STM 을 지원하는 프로그램들을 갖췄습니다.

이를 조합하여, 병행처리(concurrent processing)를 매우 단순하게 처리할 수 있으며, 대게 자동으로 처리될 수 있도록 만들 수 있습니다.

(Clojure 1.2 이상의 버전이 필요로 합니다.)

; 주석은 세미콜론(;)으로 시작합니다.

; 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.) ; <a date object>

; . 을 이용하여 메소드를 호출할 수 있습니다.
; 아니면, 줄여서 ".메소드"로도 호출 할 수 있습니다.
(. (Date.) getTime) ; <a timestamp>
(.getTime (Date.)) ; exactly the same thing.

; / 를 이용하여 정적메소드를 호출 할 수 있습니다.
(System/currentTimeMillis) ; <a timestamp> (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

읽어볼거리

부족한 것이 많았지만, 다행히도 채울 수 있는 것들이 많이 있습니다.

Clojure.org에 많은 문서들이 보관되어 있습니다: http://clojure.org/

Clojuredocs.org는 core 함수들에 대해 다양한 예제와 문서를 보유하고 있습니다: http://clojuredocs.org/quickref/Clojure%20Core

4Clojure는 clojure/FP 스킬을 올릴 수 있는 좋은 길입니다: https://4clojure.oxal.org/

Clojure-doc.org는 많고 많은 문서들을 보유하고 있습니다: http://clojure-doc.org/


제안이 있으세요? 아니면 실수를 찾으셨어요? GitHub에서 issue 또는 pull request를 만드세요

원작자는 Adam Bard, 그리고 3 기여자들 수정했다