Skip to content

Living Clojure 02

Kyunggook Kim edited this page Aug 15, 2016 · 1 revision

리스트는 클로저의 핵심이다

  • 리스트는 LISP 언어의 본질에서 유래한다.

    • LISP == LISt Processing
  • 리스트에서 인용기호가 필요한 이유

    • LISP에서는 식의 첫 요소를 연산자나 함수로 인식하기 때문
;; 인용기호가 괄호 앞에 있음으로 리스트 자료구조로써 인식됨
'("marmalade-jar" "empty-jar" "pickle-jam-jar")
;=> ("marmalade-jar" "empty-jar" "pickle-jam-jar")

;; 인용기호 없이 괄호로 시작되면 연산자나 함수로 인식됨 == 문자열은 함수가 아님
("marmalade-jar" "empty-jar" "pickle-jam-jar")
;=> ClassCastException java.lang.String cannot be cast to clojure.lang.IFn

;; 인용기호를 괄호 앞에 붙이면 결과는 숫자 2가 아닌 식을 반환
'(+ 1 1)
;=> (+ 1 1)
;; 첫째 요소는 연산자이다.
;; 둘째 요소는 정수 1이다.
;; 셋째 요소는 정수 1이다.

(first '(+ 1 1))
;=> +
  • 코드가 데이터로 취급될 수 있다!
  • 모든 클로저 코드는 데이터의 리스트로 구성되어 있다.

심볼과 바인딩의 기술

  • 클로저 심볼은 값을 가리킨다.
  • 심볼이 평가되면 그 심볼이 가리키는 값을 반환한다.
  • def는 값에 이름을 줘서 나중에 다른 곳에서 그 값을 참조할 수 있게 한다.
  • def는 심볼에 값을 직접 바인딩 하지 않고 var를 통해서 한다.
  • var는 다른 언어들에서의 변수와 다르다. 프로그램이 수행되면서 그 값이 변하지 않기 때문이다.
  • 모든 값에 대해 전역적인 var객체를 만들고 싶지 않고, 임시의 var객체 생성이 필요한 경우 let을 사용한다.
  • let을 사용하면 let 영역 안에서만 사용되는 심볼에 값을 바인딩할 수 있다.
  • let 바인딩은 벡터 안에 심볼과 값의 쌍들로 구성된다.
;; def는 REPL의 디폴트 이름공간인 user에 developer를 위한 var 객체 생성
(def developer "Alice") ;; developer의 바인딩을 "Alice"로 변경
;=> #'user/developer ;; 심볼 var 객체 생성

developer ;; 심볼을 통해 값 호출
;=> "Alice"

user/developer ;; 이름공간을 포함한 심볼을 통해 값 호출
;=> "Alice"

;; developer의 바인딩을 "Alice"로 변경
(def developer "Alice")
;=> #'user/developer ;; 심볼 var 객체 생성

;; let 안에서 developer의 바인딩을 임시로 "Alice in Wonderland"로 변경
(let [developer "Alice in Wonderland"] ;; 심볼-값 쌍
  developer) ;; 심볼을 통해 값 호출
;=> "Alice in Wonderland"

;; 기존 developer의 바인딩이 그대로 존재
developer ;; 심볼을 통해 값 호출
;=> "Alice"

;; let 안에서 일어나는 일은 let 안에서만 유효
(let [developer "Alice in Wonderland" ;; 심볼-값 쌍
      rabbit "White Rabbit"] ;; 심볼-값 쌍
  [developer rabbit]) ;; 심볼을 통해 값 호출
;=> ["Alice in Wonderland" "White Rabbit"]

;; 따라서 let 안에서 바인딩한 심볼을 let 바깥에서 참조하게 되면 에러가 발생
rabbit ;; 심볼을 통해 값 호출 시도
;=> CompilerException java.lang.RuntimeException:
;   Unable to resolve symbol: rabbit in this context
  • 심볼과 바인딩
    • 전역 var를 만들기 위해 def를 사용한다.
    • 지역 바인딩을 만들기 위해 let을 사용한다.

함수 만들기

  • 함수 만들기는 클로저 프로그램의 가장 일반적이고 중요한 부분 중 하나이다.
  • 함수를 만들고 그것에 심볼을 할당하고 나중에 그 함수를 호출할 수 있다.
  • defndef와 비슷하지만, 함수를 위한 var를 만든다.
  • defn은 함수 이름, 함수 인수들의 벡터, 함수 본문을 인수로 받는다.
  • 함수를 호출하려면 함수를 괄호로 둘러싸서 사용하면 된다.
  • 함수를 호출하면 클로저는 그 함수를 평가한 후 결과를 반환한다.
;; 빈 벡터를 이용하면 함수를 인수 없이 정의 가능
(defn follow-the-rabbit [] "Off we go!")
;=> #'user/follow-the-rabbit ;; 함수에 대한 심볼 var 객체가 생성됨

(follow-the-rabbit) ;; 심볼을 통해 함수 호출
;=> "Off we go!"

;; 인수를 받는 함수 정의(생성)
(defn shop-for-jams [jam1 jam2]
  {:name "jam-basket"
   :jam1 jam1
   :jam2 jam2})
;=> #'user/shop-for-jams ;; 함수에 대한 심볼 var 객체가 생성됨

(shop-for-jams "strawberry" "marmalade") ;; 심볼을 통해 함수에 인수를 넣어 호출
;=> {:name "jam-basket", :jam1 "strawberry", :jam2 "marmalade"}
  • 무명(익명) 함수 : 함수에 이름을 붙이지 않고 간단히 사용하고 싶은 경우 fn연산자로 만든다.
    • fn은 인수들의 벡터와 함수 본문을 받는다.
    • 무명(익명) 함수도 역시 괄호로 함수를 둘러싸서 호출하면 된다.
    • fndef로 무명 함수에 이름을 바인딩하는 것과 동일하다.
;; 무명(익명) 함수가 생성됨 == 함수를 반환
(fn [] (str "Off we go" "!"))
;=> #object[user$eval1271$fn__1272 0x45b23d29 "user$eval1271$fn__1272@45b23d29"]

;; 생성된 무명(익명) 함수를 괄호로 감싸서 호출
((fn [] (str "Off we go" "!")))
;=> "Off we go!"

;; fn은 def로 무명(익명) 함수에 이름을 바인딩하는 것과 동일
(def follow-again (fn [] (str "Off we go" "!")))
;=> #'user/follow-again // 함수에 대한 심볼 var 객체가 생성됨

(follow-again) ;; 심볼을 통해 함수 호출
;=> "Off we go!"

;; 무명(익명) 함수를 만드는 단축형. 괄호 앞에 #을 붙임
(#(str "Off we go" "!"))
;=> "Off we go!"

;; 인수가 하나 있는 경우는 퍼센트 기호(%)로 나타냄
(#(str "Off we go" "!" " - " %) "again")
;=> "Off we go! - again"

;; 인수가 여러 개라면 퍼센트 기호에 숫자를 붙여 표시
(#(str "Off we go" "!" " - " %1 %2) "again" "?")
;=> "Off we go! - again?"
  • 심볼을 관리하는 방법

    • 객체지향 언어에서는 비슷한 함수들을 묶어서 담는 객체를 사용한다.
    • 클로저는 이름공간을 사용한다.
  • 이름공간에서 심볼을 관리하기

    • 이름공간은 var에 대한 접근을 조직하고 제어하는 방법이다.
;; 디폴트 이름공간을 alice.favfoods로 변경
(ns alice.favfoods)
;=> nil

*ns* ;; 현재 이름공간을 표시
;=> #object[clojure.lang.Namespace 0x79bac443 "alice.favfoods"]

(def fav-food "strawberry jam")
;=> #'alice.favfoods/fav-food ;; 디폴트 이름공간 user가 alice.favfoods로 변경

fav-food
;=> "strawberry jam"

alice.favfoods/fav-food
;=> "strawberry jam"

;; 다른 이름공간으로 전환하면 그 심볼은 더 이상 참조되지 않음
(ns rabbit.favfoods)
;=> nil

fav-food
;=> CompilerException java.lang.RuntimeException:
;   Unable to resolve symbol: fav-food in this context

;; 전환한 다른 이름공간에서는 그 심볼을 다른 값을 지정하는 var로 정의 가능
(ns rabbit.favfoods)

(def fav-food "lettuce soup") ;; fav-food를 새로 정의
;=> #'rabbit.favfoods/fav-food

fav-food ;; rabbit.favfoods에 속한 fav-food
;=> "lettuce soup"

alice.favfoods/fav-food ;; alice.favfoods에 속한 fav-food
;=> "strawberry jam"
  • 클로저 라이브러리들은 이름공간과 그 이름공간의 심볼들로 구성된다.
  • require를 사용해서 자신의 이름공간에서 라이브러리를 사용할 수 있는 세 가지 방법이 있다.
    • 이름공간을 인수로 받아 require를 사용
    • :as를 사용해서 require의 별칭 기능을 이용
    • require를 이름공간, :refer :all 옵션과 함께 사용하면 이름공간의 모든 심볼이 로딩되고,
    • 현재의 이름공간에서 심볼 이름만으로 직접 접근할 수 있다. 이 경우 이름 충돌이 발생할 수 있다.
;; 합집합 구하기
(clojure.set/union #{:r :b :w} #{:w :p :y})
;=> #{:y :r :w :b :p}

(require 'clojure.set) ;; 1. 이름공간을 인수로 받아 심볼 로딩

(ns wonderland) ;; 이름공간 전환
;=> nil

;; 별칭 사용하기
(require '[alice.favfoods :as af])
;= nil

af/fav-food ;; 2. 별칭으로 이름공간을 로딩해서 심볼 호출
;=> "strawberry jam"

;; 보통 ns 안에서 키워드와 벡터의 형태로 사용됨
(ns wonderland
  (:require [alice.favfoods :as af]))
;=> nil

af/fav-food
;=> "strawberry jam"

;; 이름 충돌 발생 위험
(ns wonderland
  (:require [alice.favfoods :refer :all] ;; 모든 심볼 로딩
            [rabbit.favfoods :refer :all])) ;; 모든 심볼 로딩
;=> IllegalStateException fav-food already refers to:
;   #'alice.favfoods/fav-food' in namespace: wonderland
  • 대부분의 클로저 코드는 require로 라이브러리르 사용하고 :as로 별칭을 지정한다.
  • 테스트 코드 작성할 때는 테스트하려는 이름공간과 clojure.test의 함수들을 직접 사용하는 것이 일반적이다.
  • use함수는 require:refer :all과 함께 쓰는 것과 같지만 전자보다 후자가 더 좋다(?)

요약

  • 데이터 컬렉션을 만들고 조작할 수 있다.
  • 함수를 만들 수 있다.
  • 심볼을 만들 수 있다.
  • 이름공간으로 코드를 관리할 수 있다.
;; 1. clojure.set 이름공간을 로딩하고 s라는 별칭을 만들어 사용
;; 2. food1의 집합은 food-set1 심볼에 바인딩
;; 3. food2의 집합은 food-set2 심볼에 바인딩
;; 4. clojure.set 이름공간의 intersection 함수와 심볼 food-set1, food-set2를 사용
(ns wonderland
  (:require [clojure.set :as s])) ;; 1

(defn common-fav-foods [foods1 foods2]
  (let [food-set1 (set foods1) ;; 2
        food-set2 (set foods2) ;; 3
        common-foods (s/intersection food-set1 food-set2)] ;; 4
    (str "Common Foods: " common-foods)))

(common-fav-foods [:jam :brownies :toast]
                  [:lettuce :carrots :jam])
;=> "Common Foods: #{:jam}" ;; 교집합 결과 jam 반환
Clone this wiki locally