User Tools

Site Tools


lecture:clojure:시작

REPL


Reader

클로저의 코드는 클로저의 데이타로 구성된다. 클로저 코드가 실행될 때 클로저의 reader는 코드가 작성된 텍스트를 읽고, 클로져 데이타 구조로 변환한다. 그런 다음, 클로저 런타임이 그 데이타구조를 컴파일하고 실행한다.

그러나 보통의 텍스트가 바로 클로저 데이타로 사용될 수는 없다. 클로져의 reader는 텍스트를 읽어 클로저 데이타로 바꾸는 작업을 한다.

클로저 reader는 read 함수를 사용한다.

(read)
;<< ( +, 1, 1)
;=> (+ 1 1)

클로져 reader에서 리턴된 리스트 문구(form)는 바로 평가될 수 있다.

(eval (read))
;<< (+ 1 1)
;=> 2

pr은 클로저 데이타 구조를 프린트한다.

(pr (eval (read)))
;<< (+ 1 1)
;>> 6
;=> nil

위 코드를 loop를 돌리면 간단한 대화형 쉘이 된다.

(loop [] (pr (eval (read))) (recur))

REPL 실행

REPL은 파이썬이나 루비등 주로 동적 언어에서 제공되는 프로그래밍을 위한 대화형 쉘이다. 이것은 Read-Evaluate-Print-Loop의 첫 문자를 딴 단어인데, 코드를 읽고 평가하고 결과를 프린트하는 루프를 도는 과정을 통해서 프로그래머들이 대화형으로 코드를 테스트해 볼 수 있는 환경을 제공한다.

클로저의 REPL은 단순히 인터프리터가 아니라, 입력된 코드가 JVM 바이트코드로 컴파일되어 바로 실행되는데, 이것은 소스 파일에서 직접읽어 실행하는 것과 하등 차이가 없다.

다음과 같이 REPL을 실행시켜 볼 수 있다.

% java -cp clojure-1.4.0.jar clojure.main
Clojure 1.4.0
user=>

혹은

% java -jar clojure-1.4.0.jar
Clojure 1.4.0
user=>

이렇게 하면 하나의 JVM 프로세스가 시작되고, 프롬프트가 보여주고 코드 입력을 대기한다. 프롬프트는 현재의 이름 공간을 표시해 주는데, 현재 이름 공간은 user이다. REPL은 상당히 강력하고 빠른 프로그램을 개발할 수 있는 환경을 제공해 준다.

소스 파일 실행하기

클로저 소스 파일의 확장자는 .clj이다. 다음과 같이 에디터를 열고 hello.clj 파일을 편집한다.

(def message "Hello, world!")
(println message)

이 파일을 실행하는 방법은 두 가지다.

REPL에서 실행하기

REPL에서 실행하기 위해서는 REPL을 구동한 다음, 다음과 같이 한다.

(load-file "hello.clj")
>> Hello, world!
>> nil
message
>> Hello, world!

load-file은 로드할 .clj 파일의 경로를 입력받아 파일을 REPL에 로드한다. 로드하면서 마치 REPL에서 직접 입력한 것 처럼 파일의 각 문구들을 순서대로 실행한다. 정의된 모든 심볼들은 계속 유효하다.

명령창에서 실행하기

명령창을 열고 다음과 같이 입력한다.

> java -jar clojure-1.4.0.jar hello.clj
Hello, world

이렇게 하면 새로운 JVM이 실행되면서 클로저 런타임이 구동된다. 클로저 런타임은 hello.clj를 로드하고 실행한다. 이것은 자바 파일을 실행하는 것과 아주 비슷하다.

코드를 바로 실행해 볼 수도 있다.

> java -jar clojure-1.4.0.jar -e "(+ 3 4)"
7


스칼라 리터럴


스트링

스트링은 이중 따옴표로 둘러싸여 표기된다.

"hello world!"
;=> "hello world!"

다중 라인이 자동으로 된다.

"multiline strings
are very handy"
;=> "multiline strings\nare very handy"

클로져의 스트링은 자바 스트링이다(즉 java.lang.String의 인스턴스이다).

(class "hello")
;=> java.lang.String

문자

문자는 역슬래쉬로 표기된다.

(class \c)
;=> java.lang.Character

유니코드

유니코드와 8진수 표현이 가능하다.

\u00ff
;=> \ÿ
\o41
;=> \!

특수 문자 리터럴

  • \space
  • \newline
  • \forfeed
  • \return
  • \backspace
  • \tab
  • \\

논리값

true는 참이고, false는 거짓이다. nil은 false는 다르지만, 참과 거짓을 판단하는 상황에서는 false로 평가된다.

(= nil false)
;= false
(if false :a :b)
;= :b
(if nil :a :b)
;= :b

빈 리스트 ()와 0은 false가 아니다.

(if () :a :b)
;= :a
(if 0 :a :b)
;= :a

nil

nil 은 자바의 null에 해당한다. 또한 논리적으로 false와 같다. 리스프와는 다르게 nil은 ()이 아니다.

(= nil ())
;>> false

키워드

키워드는 자기자신으로 평가된다. 클로져에서는 맵이나 레코드에서 값에 대한 접근자로서 사용된다.

(def person {:name "Hong Kil-Dong" :age 23})
;=> #'user/person
(:age person)
;=> 23

키워드는
:이 앞에 붙으며 공백문자를 제외한 모든 문자로 구성된다.
/는 이름공간을 구분해 준다.
::는 현재의 이름공간내의 키워드를 의미한다.

(def person {:name "Hong Kil-Dong" :location "west" ::location "east"})
;=> #'user/person
person
;=> {:name "Hong Kil-Dong" :location "west" :user/location "east"}
(:user/location person)
;=> "east"

XML처럼 키워드가 이름공간 속에 있어서, 충돌을 방지하기 위한 밑줄이나 복잡한 도메인 모델링없이, 한 어플리케이션의 여러 모듈에서 혹은 같은 조직내에서의 별개의 그룹에서 특정 이름을 안전하게 가리킬 수 있게 된다.
name 함수로 키워드의 이름을 스트링으로 얻을 수 있으며, namespace 함수로 이름공간의 이름을 얻을 수 있다.

(name :user/location)
;=> "location"
(namespace :user/location)
;=> "user"
(namespace :location)
;=> nil

심볼

키워드와 비슷하지만, 심볼은 자기자신이 아닌 다른 값으로 평가된다. 즉 자기자신이 아닌 vars의 값이나 자바 클래스, 지역 변수 등등이다.

(def my-add (fn [x y] (+ x y)))

여기서 my-add는 var 안의 함수를 가리키는 심볼이다.
심볼은 숫자 아닌 문자로 시작해야 하며, * + ! - _ ? 등이 문자뒤에 붙을 수 있다. /이 붙으면 심볼이 속해 있는 이름공간을 나타낸다.

def 가 함수를 가르치는 경우 아래와 같이 defn 으로 약식 표기할 수 있다.

(defn my-add [x y] (+ x y))

클로져는 다양한 수 표현 능력이 있다.

리터럴 문법 수 형식
42, 0xff, 2r111, 040 long(64-bit signed integer)
3.14, 6.0221415e23 double(64-bit IEEE floating point decimal)
42N clojure.lang.BigInt(arbitrary-precision integer)
0.01M java.math.BigDecimal(arbitrary-precision signed floating point decimal)
22/7 clojure.lang.Ratio

clojure.lang.BigInt는 필요시 자동으로 java.math.BigInteger로 변환된다.

0xff와 같은 16진수 표기, 040 과 같은 8진수 표기와 더불어, 임의의 진수 표기가 가능하다. 2r111은 2진수 111을 나타내며, 16rff는 16진수 16을, 5r34는 5진수 34를 나타낸다.

N과 M을 숫자 뒤에 붙여서 임의 정밀도 수를 표기할 수 있으며, 분수 표기도 가능하다.

정수

클로저에서 정수는 이론적으로는 무한수까지 가능하지만, 시스템이 제공하는 메모리 크기에 제한된다.

42
+9
-107
99477470020757020748759207629460386871485960847968718485528475260847692104857254876204957284573785810938

다음은 진법 표기이다.

[127 0x7F 0177 16r7F 32r3V 2r01111111]
;>> [127 127 127 127 127 127]

진수(radix) 표기 방식인 16r7F는 0x7F와 같은 16진법 표기이다. 진수(radix)를 'r' 앞에 붙이고 뒤에 수를 쓴다. 진수(radix) 표기는 최대 36까지이다. 32r3V는 32진수이고 3과 V는 가갂 10진수 3과 31이다.

부동소수

1.17
+1.22
-2.
366e7
32e-14
10.7e-3

분수

클로저에서는 분수 표현이 가능하다.

22/7
7/22
7478174915757250485028195/374974857577947294847239
-103/4

분수가 딱 떨어질 때는 정수로 바뀐다. 가능다하면 약수분해를 한다.

10/2
;>> 5
10/4
;>> 5/2

정규식

문자 스트링앞에 #을 쓰면 정규표현식이 된다.

(class #"(p|h)ail")
;=> java.util.regex.Pattern
(re-seq #"(...) (...)" "foo bar")
;=> (["foo bar" "foo" "bar"])

한편으로 정규식이 하나의 리터럴로 된다는 것이 특이한데, 이로 인해 편한 점은 자바에서처럼 \를 2번 쓰지 않아도 된다는 것이다.

(re-seq #"(\d+)-(\d+)" "1-3")    ;; 자바에서는 "(\\d+)-(\\d+)" 와 같이 된다.
;=> (["1-3" "1" "3'])


코멘트


한줄 코멘트는 ;를 사용한다.

(defn sum [& r] (apply + r)) ; definition of sum
;=> 'user/sum

문구 코멘트는 리더 매크로 #_를 사용한다.

(+ 1 2 #_(* 2 2) 8)
;=> 11

클로져는 항상 괄호가 쓰이기 때문에 문구 코멘트는 매우 유용하다.

(defn some-function
  [...arguments...]
   ...code...
   (if ...debug-conditional...
    (println ...debug-info...)
    (println ...more-debug-info...)
   ...code...)

위 코드에서 if 문구을 #_ 으로 코멘트 처리하면 해당 코드 전체가 코멘트된다.

comment 매크로는 코멘트 처리에 사용될 수 있으나 항상 nil을 반환한다.

(+ 1 2 (comment (* 2 2)) 8)
;=> #<NullPointerException java.lang.NullPointerException>


공백문자와 콤마


콤마는 리더에 의해 공백문자로 취급된다.

(= [1 2 3] [1, 2, 3])
;=> true

콤마를 사용해서 더 읽기 좋아질 때만 사용하는 것이 좋다.

(create-user {:name new-username, :email email})


컬렉션 리터럴


다음은 클로져 자료구조 list, vector, map, set에 대한 리터럴들이다.

'(a b :name 12.5)           ;; list
['a 'b :name 12.5]          ;; vector
{:name "Hong" :age 23}      ;; map
#{1 2 3}                    ;; set

리스트

리스트는 다음과 같이 소괄호()로 둘러싸인다.

                                (a b c)

리스트는 여타 다른 컬렉션과는 다르게 평가된다. 리스트의 첫번째 요소는 다음과 같이 3가지로 해석되면서 나머지 요소의 해석도 달라진다. 만약 첫번째 요소가

  1. 함수 : 나머지 요소들은 각자 순서대로 평가되고, 그 결과값들이 함수의 인자로 전달된다.
  2. 매크로 : 나머지 요소들은 평가되지 않고, 매크로에서 정의된 방식으로 처리된다.
  3. 특수 문구 : 매크로와 같다.

리스트의 첫 요소가 위 세가지가 아닌 다른 문구가 되면 리스트의 평가는 실패하고 예외가 발생한다. 리스트가 첫 요소가 위와 같이 평가되지 않게 하려면 작은 따옴표 '를 괄호앞에 붙이면 된다. 이것이 리스트 리터럴을 만드는 방식이다.

(1 2 3)
;>> ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn...
'(1 2 3)
;>> (1 2 3)
 
빈 리스트 ()는 평가할 요소가 없기 때문에 '를 붙이지 않아도 된다.
 
<code clojure>
(= () '())
;= true

리스프와는 다르게 ()는 nil은 같지 않다.

(= () nil)
;= false

벡터

벡터는 다음과 같이 대괄호[]로 둘러싸인다.

               [1 2 :a :b]

벡터의 각 요소는 순서대로 평가된다.

맵은 다음과 같이 중괄호{}로 둘러싸인다.

               {1 :one, 2 :two, 3 :three}

맵은 키-값 쌍이 한 요소가 된다. 키는 유일해야 한다. 위에서 키는 1, 2, 3 정수이고, 값은 :one, :two, :three 키워드들이다. 맵의 요소들도 벡터와 같이 평가되긴 하지만, 순서는 보장되지 않는다.

집합

집합은 다음과 같이 중괄료{}로 둘러싸은 후 #을 앞에 붙인다.

               #{1 2 :one "two"}

집합은 요소들도 평가된다. 요소들은 서로 다른 값이어야 한다. 만약 평가후 같은 값이면 예외가 발생한다.

#{1 (- 2 1)}
IllegalArgumentException Duplicate key: 1      

전치 연산자

클로져에서는 함수는 항상 문구의 첫 요소가 되어야 하는데, 클로져에서는 연산자도 함수이기 때문에 항상 피연산자의 앞에 오게 된다.

(+ 1 2)
;>> 3
(< 1 2)
;>> true

연산자가 피연자 사이에 들어가는 중위 연산자에 익숙한 사람들에게는 전치 연산자가 처음에는 이상하게 보일 수도 있지만, 익숙해지면 괜찮다. 사실 전치 연산자는 더 간결하고 우아한 표현을 가능하게 한다. 다음을 보자.

(+ 1 2 3)     ; (1 + 2 + 3)
;>> 6
(< 1 2 3)     : (1 < 2) and (2 < 3)
;>> true

중위 연산자의 경우 피연자 만큼이나 반복되어 나타나지만, 전치 연산자를 처음 한 번만 나오면 된다. 또한 비교 연산자의 경우에는 위의 코드에서처럼 간결하면서도 더 의미론적 표현이 가능하다.

익명 함수 리터럴


익명 함수 리터럴(Anonymous Function Literal)은 임의로 함수를 만들어 내는 유용한 방법이다. #()를 이용한다.

(def sq #(* % %))
;=> 'user/sq
(sq 5)
;>> 25

%는 함수의 파라미터를 나타낸다. #(* % %)는 파라미터 하나를 받아 제곱을 만드는 함수이다. 이 함수를 sq라는 Var에 할당한 후 (sq 5)로 호출하여 사용하였다. 위 코드는 다음과 같이 줄일 수도 있다.

(#(* % %) 5)
;>> 25

익명 함수는 여러 인자를 받을 수 있다.

(#(* %1 %2) 5 4)
;>> 20
(#(* %1 %2 %3 %4) 5 4 3 2)
;>> 120

%1, %2, %3, %4는 익명 함수의 첫 번째, 두 번째, 세 번째, 네 번째 파라미터이다. 파라미터가 하나 밖에 없을 때는 %와 %1로 써도 되지만, 여러 개 있을 때는 이와 같이 구분해주어야 한다. 재미있는 것은 곱셉*에 인자가 여러 개 들어가고 있다는 것이다.

한가지 주의할 점은 익명 함수안에서 다시 익명 함수를 사용할 수는 없다는 것이다.

(#(+ #(* % %) #(* % %)) 3 4)
;=> fail


기타 리더 슈거


  • ' 구문의 평가를 막는다.
  • # 무명함수를 만든다.#()
  • #' var를 표시한다.
  • @ 레퍼런스의 값을 반환한다.
  • `, ~, ~@ 매크로용

quoting

클로저는 2개의 quoting 문구를 제공한다. quote와 syntax-quote. 클로저 데이타는 곧 코드라고 하였다. 즉 어떤 클로저 문구이든 바로 코드로 평가된다. 때로는 클로저 데이타를 코드로 평가하지 않고 싶을 때가 있는데, 그럴 때 qouting을 사용한다. 즉 quoting은 평가를 막는다.

quote

quote 특수 문구는 자신의 모든 인수를 평가하지 않는다.

(def x 1)
;=> #'user/x
x
;=> 1
(quote x)
;=> x
(quote (+ x 1))
;=> (+ x 1)
(= (quote (+ x 1)) '(+ x 1))
;=> true

'는 quote를 대체하는 리더 슈거이다.

syntax-quote

syntax-quote은 quote과 같다. 다만 다른 것은 심볼들이 이름 공간이 붙는다는 것이다.

'(+ x 1)
;=> (+ x 1)
`(+ x 1)
;=> (clojure.core/+ user/x 1)

unquote

quoting되는 문구중에 일부는 평가되게 하고 싶을 때 unquote를 사용한다. ~이 syntax-quote에서 사용되면 unquote된다.

`(+ 1 (* 2 3))
;=> (clojure.core/+ 1 (clojure.core/* 2 3))
`(+ 1 ~(* 2 3))
;=> (clojure.core/+ 1 6)
(let [x 2] `(1 ~x 3))
;=> (1 2 3)

unquote-splicing

(let [x '(2 3)] `(1 ~x))
;=> (1 (2 3))
(let [x '(2 3)] `(1 ~@x))
;=> (1 2 3)

'~@'에서 '@'는 시퀀스 x를 unpack하라는 것이다.

Vars

Var는 클로저에서 변화를 처리하는 한 가지 방법이다. Var는 다른 언어의 변수와 같은 것은 아니지만 비슷한 역할을 한다. 클로저에서 Var는 스레드별로 동적으로 다시 바운딩되는 변화가능한 저장 장소로서의 역할을 한다. 모든 Var는 대개 루트 바운딩되는데, 이 루트 바운딩은 모든 스레드에서 공유된다. 심볼에 의해 이름 지어진 하나의 Var는 하나의 값을 가리킨다. 이 값은 변할 수 있다.

Vars 정의하기 : def

def 특수 문구(defn, defmacro, defmulti, defonce 등 def에서 파생된 변종 문구들도 포함해서) 하나의 전역 Var 를 만든다.

만약 Var가 기존에 없다면 언바운드된 Var가 리턴된다.

(def x)
;=> #'user/x
x
#<Unbound Unbound: #'user/x>
(def x 1)
;=> 'user/x
x
=> 1

위 코드는 심볼 x와 정수 1이 바운딩해서 전역 Var인 'user/x 를 리턴한다. x를 참조하면 1이 리턴된다.

심볼의 참조는 결국 다음과 같이 Var라는 단계를 거치는 것이다.

                        Symbol ==> Var ==> Value

인터닝 Interning

이름 공간은 심볼과 Var 오브젝트들에 대한 전역 맵을 관리한다. 만약 def 문구에서 사용된 심볼이 현재의 이름 공간에서 인턴된 엔트리를 찾지 못하면 새로 만들고, 찾으면 그 심볼에 해당되는 Var를 사용한다. 이러한 과정을 인터닝이라고 한다. 인터닝된 Var는 전역이고, 인터닝되지 않은 Var는 스레드 로컬 Var이다.

Var는 변수가 아니다

Var가 사용되는 것을 보면 마치 변수같다.

(def k 10)
(def k 20)
(def m 30)

Var는 다음과 같이 전역 변수처럼 사용된다.

(def k 20)
(print k)      ; print 20
(#(println k)) ; print 20

하지만 다음을 보자

(def k 20)
(#(def k 30))
(print k)      ; print 30
(#(println k)) ; print 30

함수 안에서 k는 로컬로 형성되는 것이 아니라, k의 전역 바인딩이 바뀌게 되는 것이다. 심지어 함수 안에서 만들어진 Var도 전역이다.

(#(def m 100))
(print m) ; print 100

다음을 보자.

(def m 10)
(defn foo [m] (def m 20) (+ m 1))
(foo m) ; returns 11
(+ m 1) ; prints 21

함수 파라미터는 로컬이면서 변경 불가능이다. 따라서 함수안에서의 def 문구는 전역 Var m을 바꾸는 것이지 파라미터 m을 바꾸는 것이 아니다. foo 함수가 호출되는 함수 파라미터로 정수 10이 전달되고 함수안에서 덧셈이 수행되는 것은 이 파라미터 m에 대해서이기 때문에 결과는 11이 리턴된다.

Var 참조

var라는 특수 문구를 사용하면 Var를 직접 참조할 수도 있다. 리더 매크로 #'를 사용해도 된다.

(var k)
#'k
;=> #'user/k

Var의 이점

  • 똑같은 이름의 var가 서로 다른 이름 공간에서 사용될 수 있어, 긴 이름의 사용을 피할 수 있다.
  • 메타데이타(이후 설명)를 가질 수 있다. Var 메타 데이타는 주석, 최적화, 테스트을 포함할 수 있다.
  • 스레드별로 동적으로 다시 바인딩될 수 있다.

binding 매크로


특수 변수

REPL에는 편리한 특수 변수가 몇 개 있다.

  • *1 : 최근에 REPL에 첫 번째 입력된 표현식의 결과
  • *2 : 최근에 REPL에 두 번째 입력된 표현식의 결과
  • *3 : 최근에 REPL에 세 번째 입력된 표현식의 결과
  • *e : 최근에 REPL에 발생한 예외
(+ 1 2)
;=> 3
(+ 3 4)
;=> 7
[*1 *2]
;=> [7 3]
(1 2 3)
;>> ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn
*e
#<ClassCastException java.lang.ClassCastException: java.lang.Long cannot be cast to clojure.lang.IFn>

로컬 바인딩 let

let은 let문구가 사용되는 영역에서 참조하는 심볼을 만든다. 로컬 변수를 만드는 것과 같다.

(let [x 10
      y 20]
    (+ x y))
;=> 30

인수 분해(destructuring)때문에 바인딩 벡터는 컴파일 타임에 해석된다.

이름 공간

이름 공간은 심볼을 Var나 자바 클래스에 묶는 동적 매핑이다. 이름 공간은 자바의 패키지, 파이썬이나 루비의 모듈과 비슷한 개념이다. 클로저의 모든 코드와 데이타는 이름 공간안에서 정의된다. 기본 이름 공간은 user이다.

user=> *ns*
#<Namespace user>
user=> (def a 1)
#'user/a
user=> a
1
user=> (ns abc)
nil
abc=> *ns*
#<Namespace abc>
abc=> a
CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context
abc=> user/a
1

클로저가 제공하는 *ns*라는 Var는, 항상 현재의 이름 공간을 갖고 있다. REPL의 프롬프트는 현재의 이름 공간을 나타낸다. ns 문구는 abc라는 이름 공간(없으면 새로 만들어서)으로 전환한다. a는 user 이름 공간에서 정의되었기 때문에 abc 이름 공간에서는 찾을 수가 없다. user/a처럼 다른 이름 공간임을 명시해 주어야 한다.

user=> (+ 1 1)
2
user=> (in-ns 'abc)
#<Namespace abc>
abc=> (+ 1 1)
CompilerException java.lang.RuntimeException: Unable to resolve symbol: + in this context
abc=> (clojure.core/+ 1 1)
2

ns는 이름 공간을 바꾸는 매크로인데 반해, in-ns는 같은 역할을 하는 함수이다. in-ns의 경우 파라미터에 '를 붙여서 평가되지 않게 해야 한다. 이름 공간이 바뀌면 단지 특수 문구만이 기본으로 정의되어 있을 뿐이다. 다른 이름 공간으로부터 코드를 로드하고 vars를 새로운 이름 공간으로 매핑해야 한다.

abc=> (clojure.core/refer 'user)
nil
abc=> a
1

함수만들기

fn

fn은 함수를 만드는 특수 문구이다.

(fn [x] 
  (+ 10 x))

함수의 fn 다음에 파라미터를 기술하는 바인딩 벡터가 오고, 그 뒤로 함수의 몸체 문구가 온다. 몸체에는 문구가 여러 개 올 수 있는데 제일 마지막 문구의 평가값이 함수의 리턴값이다. 아래는 여러 개의 파라미터를 갖는 함수 정의이다.

((fn [x y x] 
  (+ x y z)) 1 2 3)
;=> 6

다음은 여러 개의 파라미터 목록(multiple arities)를 갖는 함수 정의이다.

(def my-sub (fn  
               ([x y] (- x y))
               ([x y z] (- (+ x y) z))))
;=> #'user/my-sub
(my-sub 10 3)
;=> 7
(my-sub 10 5 3)
;=> 12

defn

클로저에서 함수를 만드는 기본적인 방법은 defn을 사용하는 것이다.

(defn my-add [x y z]
  (+ x y z))
 
(defn my-sub 
  ([x y] (- x y))
  ([x y z] (- (+ x y) z))))

do 코드 블럭

여러 개의 문구를 한 단위로 평가하고 싶을 때 do 코드 블럭을 사용한다.

(do
  (println "hi")
  (+ 1 2))
;>> hi
;=> 3

함수는 do 코드 블럭이 내부적으로 사용된다. 하지만 익명 함수 리터럴의 경우 명시적으로 do를 사용해야 한다.

(defn f [x y]
  (println "hi")
  (+ x y))
 
#(do (println "hi")
     (+ %1 %2))

임의의 수의 파라미터 정의 (variadic function)

(defn my-add [x & rest]
  (- x (apply + rest)))
;=> #'user/my-add
(my-add 10 1 2 3)
;=> 4
(my-add 10 1 2 3 4)
;=> 0
(#(- % (apply + %&)) 10 1 2 3 4)
;=> 0

조건 문구 if

if는 클로저에서 제공하는 조건 판단하는 유일한 특수 문구이다.

                    (if test then else?)

if 특수 문구 다음에 나오는 test 문구를 평가하는데, 만약 test가 nil이나 false가 아니면, then 문구만 평가한다. 만약 test 문구가 nil이나 false일 때, else 문구가 없으면 nil을 반환하고, else 문구가 있으면 else 문구를 평가한 결과를 리턴한다.

클로저의 다른 조건 판단 문구들은 모두 if에 기반해 있기 때문에, nil과 false가 아닌 모든 값들에 대해서는 true로 처리한다.

(if "hi" \t)        ;=> \t
(if 42 \t)          ;=> \t
(if nil \t \f)      ;=> \f
(if false \t)       ;=> nil
(if (not true) \t)  ;=> nil

if외에 아래와 같은 조건 판단 문구가 있다.

  • when : 조건이 거짓일 때, 반드시 아무 동작도 하지 않고 nil을 반환해야 할 때 사용하면 좋다.
  • cond : 자바의 switch-case 구문과 같은 역할을 한다. condp는 조건 판단을 위한 predicate를 사용한다.
  • if-let과 when-let : let과 if, when이 결합된 것인데, 조건이 참일 때 let 바인딩이 성립하고 처리된다.

루핑 : loop 과 recur

recur 특수 문구는 다음과 같이 사용된다.

                        (recur exprs*)

recur 다음에 문구들이 아예 없거나 여러 개 올 수 있는데, 모두 순서대로 평가된다. 평가 이후 제어는 가장 최근 재귀점으로 돌아가는데, 재귀점은 함수이거나 loop 문구이다. 평가된 결과값들은 재귀점이 함수일 경우 함수 파라미터들과 다시 바인딩되고, 재귀점이 loop 인 경우에는 loop 바인딩에 다시 바인딩된다. 바인딩시 바인딩 순서가 매치되어야 한다.

(loop [x 5]
  (if (neg? x)
    x
    (recur (dec x))))
;=> -1

위 코드에서 loop은 recur를 위한 재귀점이면서, 초기값을 설정하는 바인딩 벡터를 갖는다. x는 처음 5가 된다. if 문구는 loop을 빠져나오는 조건 판단을 제공하는 것으로, 이것은 반드시 있어야 하는데, 없으면 무한 루프가 되기 때문이다. recur 문구에서 dec 문구에서 x가 하나 줄어든 값으로 평가되어, 이 값은 4가 되는데, 이것은 제어가 재귀점인 loop으로 이동하면서 loop 의 바인딩에 다시 바인딩된다. 그래서 이번에는 x가 4가 된다. 이런 식으로 반복되다가 if 문구에서 x가 -1 일 때 loop를 빠져나오면서 x를 반환하게 된다. 여기서 주의할 점은 로컬 심볼 x는 중간에 변경되지는 않았다는 것이다.

다음은 recur의 재귀점이 함수인 경우이다.

(defn countdown [x]
  (if (zero? x)
    :blastoff!
    (do (println x)
        (recur (dec x)))))
;= #'user/countdown
(countdown 5)
;>> 5
;>> 4
;>> 3
;>> 2
;>> 1
;=> :blastoff!

recur는 스택 공간을 전혀 사용하지 않는다. 꼬리 호출(tail-call) 최적화 1)는 되지 않는다. recur 호출은 꼬리 호출이어야 한다. 꼬리 호출이 아닌 recur 호출은 컴파일러에 의해 에러로 잡힌다.

(fn [x] (recur x) (println x))
; java.lang.UnsupportedOperationException: Can only recur from tail position

자바 상호운영성 Java Interop

클로저는 자바 객체 생성, 정적 메소드 및 개체 메소드 호출, 개체 필드 접근 등을 클로저 코드에서 직접 할 수 있는 강력한 상호운영성을 제공한다. 특수 문구 '.', new, set! 등이 제공된다. 하지만 이것보다는 리더에서 제공하는 슈거가 더 많이 사용된다.

Operation Java code Sugared interop form Equivalent special form usage
Object instantiation new java.util.Array.List(100) (java.util.ArrayList. 100) (new java.util.ArrayList 100)
Static method call Math.pow(2, 10) (Math/pow 2 10) (. Math pow 2 10)
Instance method call “hello”.substring(1, 3) (.substring “hello” 1 3) (. “hello” substring 1 3)
Static field access Integer.MAX_VALUE Integer/MAX_VALUE (. Integer MAX_VALUE)
Instance field access someObject.someField (.someField someObject) (. someObject someField)

클로저는 변경 불가능을 강조하지만 자바 클래스나 객체의 필드를 설정을 위해서 set!을 제공한다. 이에 대해서는 자바 상호운용성 장에서 자세히 알아본다.

1)
함수를 빠져나오는 곳을 '꼬리'라고 하는데, 이 꼬리 지점에서 다른 함수를 호출하는 것을 '꼬리 호출'이라고 한다. 일반적으로 함수 호출은 스택 프레임을 만들고 피호출 함수로 제어를 이전한 후 다시 스택 프레임이 제거하고 호출 지점으로 돌아온다. 꼬리 호출시에는 스택 프레임을 만들지 않고 피호출 함수로 이동하는데, 이렇게 되면 호출 함수의 스택 프레임을 피호출 함수가 그대로 사용하게 되어, 낭비가 줄어든다. 이것은 GOTO를 사용한 것과 같은 효과를 나타낸다.
lecture/clojure/시작.txt · Last modified: 2019/02/04 14:26 (external edit)