Skip to content

Instantly share code, notes, and snippets.

@seeeturtle
Created February 2, 2019 07:52
Show Gist options
  • Save seeeturtle/bbef144ecc3ceba874c04656ab873b61 to your computer and use it in GitHub Desktop.
Save seeeturtle/bbef144ecc3ceba874c04656ab873b61 to your computer and use it in GitHub Desktop.
클로저 프로그래밍 Chapter1

fn - 익명 함수

clojure

(fn [x]
  (+ 10 x))

python

lambda x: 10 + x
  • 인자에 구조 분해(destructing) 적용할 수 있다.
  • 함수의 본문은 일종의 do문으로 감싸져 있다.
  • 즉 여러 개의 표현식이 들어갈 수 있고, 마지막 값이 반환값이다.

호출할때 인자는 함수 선언할때의 인자(혹은 구조분해)의 위치에 따라 들어간다.

((fn [x
      [a b c :as v]]
   (list x a b c v))
 10 [1 2 3])

그리고 이 호출은 다음 let form과 동일하다

(let [x 10
      [a b c :as v] [1 2 3]]
  (list x a b c v))

multiple arities

multiple arities 함수도 만들 수 있다. (multiple arities, 즉 함수의 인자의 개수가 여러개일 수 있다.)

(def add-or-inc (fn self ;; 익명함수에 이름도 붙일 수 있다!
                   ([x] (self x 1)) ;; var에 직접 접근할 필요없이 호출할 수 있다
                   ([x y] (+ x y))))

(list (add-or-inc 10)
      (add-or-inc 10 20))

mutual recursion

mutual recursion이란 간단하게 설명해서 두개의 함수가 서로를 호출 하는 재귀를 말한다. 예를 들면 0 이상의 정수 n에 대하여 짝수인지 홀수인지 판별하는 함수를 만들어보자.

짝수와 홀수가 번갈아가면서 나타난다는 사실을 이용하면 쉽게 떠올릴 수 있다. 짝수인지 판별하는 함수 E(n)과 홀수인지 판별하는 함수 O(x)가 있다고 하자.

  1. n이 0이라면 E(n)은 참을 반환하고, O(n)은 거짓을 반환하다.
  2. n이 홀수라면 E(n-1)은 참을 반환할 것이다.
  3. n이 짝수라면 O(n-1)은 거짓을 반환할 것이다.

이게 바로 mutual recursion이다. 위키피디아 참고

letfn & mutual recursion

그렇다면 이를 어떻게 구현할까?

letfn은 이름을 가진 여러개의 함수들을 한번에 정의할 수 있게해준다. 그러므로 letfn을 이용한다면 mutual recursion을 쉽게 구현할 수 있다.

(letfn
    [(odd? [n]
       (if (zero? n)
         false
         (even? (- n 1))))
     (even? [n]
       (if (zero? n)
         true
         (odd? (- n 1))))]
  (even? 4))

let은?

여기서 드는 의문점은 왜 let을 사용할 수 없을까?이다.

(let [f (fn [n]
             (if (zero? n)
               false
               (g (- n 1))))
      g (fn [n]
              (if (zero? n)
                true
                (f (- n 1))))]
  (f 5))

위 코드를 실행해보면 문제점을 알 수 있다. let은 순차적으로 평가하고 이름에 묶어준다. 하지만 f의 값을 평가할 때에 g는 아직 정의되지 않았다. 그러므로 에러가 나게 된다.

하지만 letfn은 저 안에서 정의되어있다면 호출해도 문제가 나지 않는다.

defn

  • defn은 def와 fn의 기능을 합친 일종의 매크로이다.
  • 이름이 있고 현 네임스페이스에 그 이름으로 등록된 함수를 만들어준다.
(defn foo [] :foo)
(def foo (fn foo [] :foo))
  • 파이썬의 람다는 네임스페이스에 등록을 할 수는 있지만 함수의 이름을 바로 지어주지는 못한다.
def foo:
    return 'foo'

foo = lambda : 'foo'

함수 인자의 구조 분해

  • let에서 구조분해는 함수에서도 똑같이 적용된다고 볼 수 있다.
  • let에서 남은 것들을 한 시퀀스로 저장하는 걸 함수에서도 똑같이 사용할 수 있다.
((fn [x & rest] (list x rest)) 1 2 3)

(let [[x & rest] [1 2 3]]
  (list x rest))

python:

def x_rest(x, *rest):
    return (x, rest)

x_rest(1, 2, 3)

키워드 인자

  • 인자의 나머지 시퀀스에서 한번 더 맵 구조 분해를 하는 것을 상상해볼 수 있다.
((fn [x & {:keys [a b]}] (list x a b)) 1 :a "a" :b "b")

(let [[x & {:keys [a b]}] [1 :a "a" :b "b"]]
  (list x a b))

python:

def x_keys(x, *, a, b): return (x, a, b)

x_keys(1, a="a", b="b")

다른 키들

  • 더 재밌는 점은 이게 구조분해를 사용하는 것이기 때문에 key가 무엇이든 상관없다!
(let [[x & {k "key"}] [1 "key" "value"]]
  (list x k))

((fn [x & {k "key"}]
   (list x k)) 1 "key" "value")
  • 다만 맵에서 키로 쓰는 것은 대부분 키워드이므로 키워드 인자라고 부른다.

함수 리터럴

(fn [x] (/ x 2))
#(/ % 2)

(fn [x y z] (+ x y z))
#(+ %1 %2 %3)

'#(+ %1 %2)

함수 리터럴의 인자는 고유의 심볼을 만들어서 사용한다.

no implicit do form!

함수 리터럴은 암묵적으로 do form으로 싸주지 않는다.

(fn []
  (println "blah")
  :return)

#(do (println "blah")
     :return)

추가적인 규칙들…

  1. %와 %1은 같은 인자를 가리킨다.
'#(+ % %1)

추가적인 규칙들…

  1. % 뒤에 숫자가 몇번째 인자인지 가리킨다.
(#(list %1 %2 %3) 1 2 3)

추가적인 규칙들…

  1. 가장 큰 숫자가 인자의 개수를 정한다.
'#(list %1 %3)

추가적인 규칙들…

  1. %&는 나머지 인자들을 가리킨다.
(#(list % %&) 1 2 3 4)

추가적인 규칙들…

  1. nested 함수 리터럴은 안된다. - 어떤 인자가 어떤 함수의 인자를 가리키는 모르므로.
(#(#(- % %)) 1) ;; 1은 앞 %에 들어갈까? 뒤 %에 들어갈까?

if form

(if test then else)

  • nil, false를 제외한 나머지는 모두 논리적으로 참 이라 여긴다.
  • 만약 test가 논리적으로 참 이라면 then의 값을 반환한다.
  • 아니라면, else의 값을 반환한다. (else가 주어졌을때에)
  • else가 없다면 nil을 반환한다.
(if "str" \t) ;= \t

(if 100 \t) ;= \t

(if nil \t \f) ;= \f

(if false \t \f) ;= \f

(if false \t) ;= nil

다른 조건문들

when form

(when test
  then1
  then2
  ...)

(if test
  (do
    then1
    then2
    ...))
  • 조건이 거짓이라면 nil을 반환.
  • if와 다른 점은? 암묵적으로 do로 싸여저있다.
  • 즉 여러개의 표현식이 들어갈 수 있다.
  • if는 하나밖에 못 들어간다.

loop & recur

  • recur는 가장 가까운 recursion point로 새로운 상태로 바꾸어서 전달한다.
  • 여기서 recursion point는 loop나 함수를 같을 걸 얘기한다.
(loop [x 1] ;; recursion point
  (if (> x 10)
    x
    (recur (inc x)))) ;; 다시 위로 x를 1 늘려서 전달

(defn countup [x] ;; recursion point
  (if (> x 10)
    x
    (recur (inc x))))

(countup 1)
(countup 2)
...
(countup 10)
10

왜, 언제 쓸까?

  • 성능이 필요할때. recur는 스택을 쌓지 않는다.
  • 숫자 연산에서도 더 나은 성능을 가진다. (boxed represantation을 사용하지 않아서)
  • map, reduce 와 같은 걸로는 쉽게 다루기 힘들때에

클로저의 숫자에 대한 얘기는 챕터 11에서 더 자세하게 다루어진다.

boxed represantation?

자바에는 primitive type과 boxed primitive가 있다. primitive type은 객체가 아니고 저수준 타입에 해당하는 타입이다. boxed primitive는 이런 primitive type을 객체로 싸아서 객체처럼 다루어진다.

다만 boxed primitive는 실제 연산할 때에는 다시 unbox되는 과정이 필요하기 때문에 그냥 primitive type보다 느리다.

하지만 recur를 사용하거나 타입을 지정해준다면 prmitive type을 직접 사용할 수 있다. boxed math

var

var의 이름인 심볼은 평가되었을 때에 var의 값을 반환한다.

하지만 var 자체를 가져오고 싶다면 var이나 #’ reader macro를 사용하면 된다.

(def x 1)

(var x)
#'x

Java Interop

  • 기본적으로는 . 와 new 가 있지만
  • 더 나은 syntax sugar들이 있다!

try & throw

set!

  • 기본적으로 클로저의 데이터 타입들은 불변이지만
  • 자바 객체의 필드를 바꾸는 등 경우에는 set! 을 사용할 수 있다.

locking

다시 average 함수로

(defn average [number]
  (/ (apply + numbers) (count numbers)))

(def average (fn average [number]
               (/ (apply + numbers) (count numbers))))

eval

이런 평가하는 것들은 eval로 할 수 있다.

앞서서 얘기한 read와 합친다면 쉽게 클로저 REPL를 다시 만들 수 도 있다.

(defn my-repl []
  (print (str (ns-name *ns*) ">>> "))
  (flush)
  (let [expr (read)
        value (eval expr)]
    (when (not= :quit value)
      (println value)
      (recur))))

다만 eval 자체는 클로저에서 사용하는 경우는 거의 없다. eval로 해결할 수 있는 것들은 대부분 매크로로 해결할 수 있다.

개인적으로 도움이 되었던 곳

  • 스택오버플로우 님은 언제나 도움이 되신다.
  • clojurian slack도 항상 빠르고 좋은 답변으로 도움이 많이 되었다. https://clojurians.herokuapp.com/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment