Home [Clojure] Do Things: A Clojure Crash Course - Functions
Post
Cancel
Preview Image

[Clojure] Do Things: A Clojure Crash Course - Functions

사람들이 Lisps에 열광하는 이유 중 하나는 이러한 언어를 사용하면 복잡한 방식으로 동작하는 프로그램을 만들 수 있지만 주요 구성 요소인 함수가 매우 단순하기 때문이다.

이 섹션에서는 다음 내용을 설명함으로써 Lisp 함수의 아름다움과 우아함을 소개한다.

  • 함수 호출 (Calling Functions)
  • 어떻게 함수가 macros와 특별한 forms과 다른지
  • 함수의 정의 (Defining Functions)
  • 익명 함수 (Anonymous Functions)
  • 함수 반환 (Returning Functions)

Calling Functions

우리는 다양한 예시의 함수 호출을 봐왔다.

1
2
3
(+ 1 2 3 4)
(* 1 2 3 4)
(first [1 2 3 4])

모든 Clojure 연산은 같은 문법인 것을 기억하자.

여는 괄호(, 연산자, 피연산자, 닫는 괄호)

함수 호출은 그저 연산자가 함수이거나 함수형인 연산의 다른 용어일 뿐이다.

이것은 우리에게 몇몇 흥미롭고 아름다운 코드를 작성하게 해준다.

아래 함수는 + (addition) 함수를 리턴하는 함수 표현이다.

1
2
(or + -)
; => #<core$_PLUS_ clojure.core$_PLUS_@76dace31>
  • 왜냐하면, or 은 첫번째 truthy 값을 리턴하는데, + 함수는 truthy 하기 때문에

따라서 아래와 같이 유효한 함수표현들을 쓸 수도 있다.

1
2
3
4
5
6
7
8
((or + -) 1 2 3)
; => 6

((and (= 1 1) +) 1 2 3)
; => 6

((first [+ 0]) 1 2 3)
; => 6

그러나 유효하지 않은 함수 호출도 존재하는데, 숫자나 문자열은 함수가 아니기 때문이다.

1
2
(1 2 3 4)
("test" 1 2 3)

위의 표현을 REPL에서 실행하면 아래와 같이 에러가 뜨는 것을 볼 수 있다.

1
2
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn
user/eval728 (NO_SOURCE_FILE:1)

Clojure를 계속 하면서 <x> cannot be cast to clojure.lang.IFn 와 같은 에러를 자주 보게 될 텐데,

이 에러의 의미는 단지 함수가 아닌 것을 함수로 사용하려고 한다는 의미다.

함수의 유연성은 함수 표현으로 끝나지 않는다.

  • 구문적으로 함수는 다른 함수를 포함하여 모든 표현식을 인수로 사용할 수 있다.
  • 함수를 인수로 취하거나, 함수를 반환할 수 있는 함수를 “고차 함수”라고 한다.
  • 고차 함수를 사용하는 프로그래밍 언어는 일급 함수(first-class functions)를 지원한다고 한다.
  • 함수를 값으로 처리할 수 있기 때문에

예를 들어 map 함수를 보면, 컬렉션의 각 멤버에 함수를 적용한 리스트를 새로 생성해서 리턴한다.

inc 함수는 숫자를 1 증가시키는 함수다.

1
2
3
4
5
(inc 1.1)
; => 2.1

(map inc [0 1 2 3])
; => (1 2 3 4)

(여기서 map 은 벡터를 인수로 사용했지만, 벡터를 리턴하는게 아니다. 이유는 나중에 배우게 된다.)

일급 함수에 대한 Clojure의 지원을 통해서 함수가 없는 언어에서보다 더 강력한 추상화를 구축할 수 있다.

  • 이런 종류의 프로그래밍에 익숙하지 않은 사람들은 함수를 데이터 인스턴스에 대한 작업을 일반화 할 수 있는 것으로 생각한다.
  • 예를 들어, 이 + 함수는 특정 숫자에 대한 덧셈을 추상화한다.

대조적으로 Clojure(및 모든 Lisp)를 사용하면, 프로세스를 일반화하는 기능을 만들 수 있다.

map 은 모든 컬렉션에 함수를 적용하여, 컬렉션을 변환하는 프로세스를 일반화할 수 있다.

함수 호출에 대해 알아야 할 마지막 세부사항은 Clojure가 함수에 전달하기 전에 모든 함수 인수를 재귀적으로 수행한다는 것이다.

다음은 Clojure가 어떻게 인수가 함수 호출이기도 한 함수 호출에 대해서 수행하는 방법이다.

1
2
3
4
5
(+ (inc 199) (/ 100 (- 7 2)))
(+ 200 (/ 100 (- 7 2))) ; evaluated "(inc 199)"
(+ 200 (/ 100 5)) ; evaluated (- 7 2)
(+ 200 20) ; evaluated (/ 100 5)
220 ; final evaluation

함수 호출은 수행 프로세스를 시작하고, 모든 subform 들은 + 함수에 적용되기 전에 수행된다.

Function Calls, Macro Calls, and Special Forms

이전 섹션에서 함수 호출은 함수 표현식을 연산자로 갖는 표현식이라는 것을 배웠다.

두 가지 다른 종류의 표현식은

  • 매크로 호출 (macro calls)
  • 특별한 폼 (special forms)
    • Clojure에서는 form이 중요한 표현이기 때문에, 형식이라기 보다 폼이라는 용어를 그대로 사용함으로써 구별하려고 한다.

이다.

우리는 이미 if 처럼 몇 가지 특별한 폼을 봤었다.

7장에서 매크로 형식와 특별한 폼에 대해 알아야 할 것들을 배울 것이고,

지금은 특별함 폼을 특별하게 만드는 주요 기능은 함수 호출과 달리, 항상 모든 피연산자들을 수행하지 않는 것이다.

아래는 if 의 일반적인 구조다.

1
2
3
(if boolean-form
  then-form
  optional-else-form)

이제 아래와 같이 선언했다고 가정하자.

1
2
3
(if good-mood
  (tweet walking-on-sunshine-lyrics)
  (tweet mopey-country-song-lyrics))

우리는 확실하게 Clojure 가 두 가지 중 하나만 수행하기를 원한다.

  • 만약 Clojure가 두 tweet 함수 모두를 수행했다면, 트위터 팔로워들은 매우 혼란스러워 할 것이다.

특별한 폼을 구별하는 또 다른 기능은 함수에 대한 인수로 사용할 수 없다는 것이다.

  • 일반적으로 특별한 폼은 기능으로 구현할 수 없는 핵심 Clojure 기능을 구현한다.
  • Clojure에는 소수의 특별한 폼들만 있으며, 이렇게 풍부한 언어가 작은 구성 요소 집합으로 구현된다는 것이 매우 놀랍다.

매크로는 함수 호출과 다르게 피연사자를 수행하고 함수에 인수로 전달할 수 없다는 점에서 특별한 폼과 유사하다.

이제 돌아가는건 오래 걸렸으니, 함수를 정의하는 방법을 배워보자!

Defining Functions

함수 정의는 다음 다섯가지 메인 파트로 구성된다.

  • defn
  • 함수 이름
  • 함수를 설명하는 docstring (optional)
  • 대괄호안에 나열된 파라미터들
  • 함수 본문
1
2
3
4
5
6
7
8
(defn too-enthusiastic
	"Return a cheer that might be a bit too enthusiastic"
	[name]
	(str "OH. MY. GOD! " name " YOU ARE MOST DEFINITELY LIKE THE BEST "
  "MAN SLASH WOMAN EVER I LOVE YOU AND WE SHOULD RUN AWAY SOMEWHERE"))

(too-enthusiastic "Zelda")
; => "OH. MY. GOD! Zelda YOU ARE MOST DEFINITELY LIKE THE BEST MAN SLASH WOMAN EVER I LOVE YOU AND WE SHOULD RUN AWAY SOMEWHERE"

docstring

docstring은 코드를 설명하고 문서화 하는 유용한 방법

doc 함수를 통해서 REPL에서 함수의 docstring을 볼 수 있다.

코드에 대한 문서를 생성하는 도구를 사용하는 경우에도 docstring이 작동한다.

Parameters and Arity

Clojure의 함수들은 0개 이상의 매개변수들로 정의할 수 있다.

함수를 통과하는 값들을 인수들(arguments)라고 부르고, 인수들은 임의의 타입이 될 수 있다.

매개변수들의 개수를 함수의 Arity 라고 한다.

1
2
3
4
5
6
7
8
9
10
(defn no-params
  []
  "I take no parameters!")
(defn one-param
  [x]
  (str "I take one parameter: " x))
(defn two-params
  [x y]
  (str "Two parameters! That's nothing! Pah! I will smoosh them "
  "together to spite you! " x y))
  • no-params → 0-arity function
  • one-param → 1-arity function
  • two-params → 2-arity function

함수는 arity 오버로딩(arity overloading)을 지원한다. 즉, arity에 따라 다른 함수 본체가 실행되도록 함수를 정의할 수 있다.

multiple-arity 함수 정의의 일반적인 형태는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
(defn multi-arity
  ;; 3-arity arguments and body
  ([first-arg second-arg third-arg]
     (do-things first-arg second-arg third-arg))
  ;; 2-arity arguments and body
  ([first-arg second-arg]
     (do-things first-arg second-arg))
  ;; 1-arity arguments and body
  ([first-arg]
     (do-things first-arg)))

arity 오버로딩은 인수에 기본값을 제공하는 한 가지 방법이다.

아래 예시는 chop-type 파라미터의 기본 값으로 “karate”가 기본값인 경우다.

만약 파라미터 값이 2개이면 원래대로, 1개이면 기본 값으로 적용된다.

1
2
3
4
5
6
7
8
9
10
11
12
(defn x-chop
  "Describe the kind of chop you're inflicting on someone"
  ([name chop-type]
     (str "I " chop-type " chop " name "! Take that!"))
  ([name]
     (x-chop name "karate")))

(x-chop "Kanye West" "slap")
; => "I slap chop Kanye West! Take that!"

(x-chop "Kanye East")
; => "I karate chop Kanye East! Take that!"

Arity 값에 따라 완전히 관계없는 것을 할 수 있게 만들 수도 있다.

1
2
3
4
5
6
7
8
(defn weird-arity
  ([]
     "Destiny dressed you this morning, my friend, and now Fear is
     trying to pull off your pants. If you give up, if you give in,
     you're gonna end up naked with Fear just standing there laughing
     at your dangling unmentionables! - the Tick")
  ([number]
     (inc number)))

가능은 하지만… 굳이 이렇게 쓸 필요는 없는 듯 하다.

그런데 리턴타입이 없나…?

Clojure를 사용하면, rest parameter를 포함해서 가변 변수 함수(variable-arity functions)를 정의할 수 있다.

rest parameter는 & 로 표시된다.

1
2
3
4
5
6
7
8
9
10
11
12
defn codger-communication
  [whippersnapper]
  (str "Get off my lawn, " whippersnapper "!!!"))

(defn codger
   [& whippersnappers]
  (map codger-communication whippersnappers))

(codger "Billy" "Anne-Marie" "The Incredible Bulk")
; => ("Get off my lawn, Billy!!!"
      "Get off my lawn, Anne-Marie!!!"
      "Get off my lawn, The Incredible Bulk!!!")

위에서 호출될때 알 수 있다시피, 가변 인수를 제공할 때 인수는 목록으로 처리된다.

rest parameters를 일반 parameters와 섞을 수 있지만, 그럴 땐 마지막에 와야 한다.

(다른 언어와 동일한 부분이다.)

1
2
3
4
5
6
7
(defn favorite-things
  [name & things]
  (str "Hi, " name ", here are my favorite things: "
       (clojure.string/join ", " things)))

(favorite-things "Doreen" "gum" "shoes" "kara-te")
; => "Hi, Doreen, here are my favorite things: gum, shoes, kara-te"

마지막으로 Clojure는 자체적으로 하위섹션(subsection)이 필요한 Destructuring 이라는 매개변수를 정의하는 보다 정교한 방법이 있다.

Destructuring

분해, 비구조화라고 한다.

기본 개념은 컬렉션 내의 값에 이름을 간결하게 바인딩할 수 있다는 것이다.

1
2
3
4
5
6
7
;; Return the first element of a collection
(defn my-first
  [[first-thing]] ; Notice that first-thing is within a vector
  first-thing)

(my-first ["oven" "bike" "war-axe"])
; => "oven"

여기에서 my-first 라는 함수는 first-thing 를 인수로 전달된 벡터의 첫 번째 요소와 연결한다.

백터내에 first-thing 기호를 배치하여 이를 수행하도록 지시한 것

벡터 또는 목록을 비구조화할 때 원하는 만큼 요소의 이름을 지정할 수 있고, 나머지 매개변수도 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
(defn chooser
  [[first-choice second-choice & unimportant-choices]]
  (println (str "Your first choice is: " first-choice))
  (println (str "Your second choice is: " second-choice))
  (println (str "We're ignoring the rest of your choices. "
                "Here they are in case you need to cry over them: "
                (clojure.string/join ", " unimportant-choices))))

(chooser ["Marmalade", "Handsome Jack", "Pigpen", "Aquaman"])
; => Your first choice is: Marmalade
; => Your second choice is: Handsome Jack
; => We're ignoring the rest of your choices. Here they are in case \
     you need to cry over them: Pigpen, Aquaman

위에서 나머지 매개변수(rest parameter) unimportant-choices 은 첫 번째와 두 번째 이후에 사용자가 추가로 선택할 수 있는 모든 것을 처리한다.

맵을 매개변수로 제공해서 비구조화할 수도 있다.

1
2
3
4
5
6
7
8
(defn announce-treasure-location
   [{lat :lat lng :lng}]
  (println (str "Treasure lat: " lat))
  (println (str "Treasure lng: " lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})
; => Treasure lat: 28.22
; => Treasure lng: 81.33

lat 이라는 이름과 키 :lat 에 해당하는 값을 연결, lng 이라는 이름과 키 :lng 에 해당하는 값을 연결

종종 맵에서 키워드를 분리하기를 원하므로 아래와 같이 쓸 수도 있다.

1
2
3
4
(defn announce-treasure-location
  [{:keys [lat lng]}]
  (println (str "Treasure lat: " lat))
  (println (str "Treasure lng: " lng)))

:as 키워드를 사용해서 원래 맵 인수에 대한 액세스도 유지할 수 있다.

1
2
3
4
5
6
7
8
9
(defn receive-treasure-location
  [{:keys [lat lng] :as treasure-location}]
  (println (str "Treasure lat: " lat))
  (println (str "Treasure lng: " lng))

  ;; One would assume that this would put in new coordinates for your ship
  (steer-ship! treasure-location))

;; 위의 예시가 실제로 어떻게 동작하는지 이해 안 감. repl에서 동작도 하지 않음

일반적으로 비구조화는 리스트, 맵, 집합 또는 벡터의 값과 이름을 연결하는 방법에 대해 Clojure가 지시하는 것으로 생각할 수 있다.

Function Body

함수의 본문은 모든 종류의 form을 포함할 수 있다.

Clojure는 평가된(evaluated) 마지막 form을 자동으로 반환한다.

1
2
3
4
5
6
7
8
(defn illustrative-function
  []
  (+ 1 304)
  30
  "joe")

(illustrative-function)
; => "joe"
1
2
3
4
5
6
7
8
9
10
11
(defn number-comment
  [x]
  (if (> x 6)
    "Oh my gosh! What a big number!"
    "That number's OK, I guess"))

(number-comment 5)
; => "That number's OK, I guess"

(number-comment 7)
; => "Oh my gosh! What a big number!"

All Functions Are Created Equal

모든 함수들은 동일하게 생성된다.

마지막 참고 사항: Clojure에는 privileged functions가 없다.

+ 는 그저 함수일 뿐이고, - 도 그저 함수일 뿐이고, incmap 도 그저 함수일 뿐이다.

스스로 정의한 함수보다 나을 것이 없다.

더 중요한 것은 이 사실이 Clojure의 기본 단순성을 입증하는데 도움이 된다는 것이다.

어떤 면에서는 Clojure는 매우 멍청하다.

함수가 어디에서 왔는지는 중요하지 않고, 모든 함수를 동일하게 취급한다.

함수를 적용하는 것에만 관심이 있다.

계속해서 Clojure로 프로그래밍하면, 이러한 단순함이 이상적이라는 것을 알게 될 것이다.

다른 함수로 작업하기 위한 특별한 규칙이나 구문에 대해 걱정할 필요가 없다.

그들은 모두 동일하게 작동한다.

Anonymous Functions

Clojure에서는 함수의 이름이 필요하지 않다.

사실, 우리는 항상 anonymous 함수를 사용하게 될 것이다.

다음 두 가지 방법으로 익명 함수를 만든다.

첫 번째는 다음 fn 형식을 사용하는 것이다.

1
2
(fn [param-list]
  function body)
1
2
3
4
5
6
(map (fn [name] (str "Hi, " name))
     ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

((fn [x] (* x 3)) 8)
; => 24

defn 과 매우 유사하며, 동일하게 다루면 된다.

매개변수 목록과 함수 본문은 정확히 동일하게 작동한다.

1
2
3
(def my-special-multiplier (fn [x] (* x 3)))
(my-special-multiplier 12)
; => 36

Clojure는 익명 함수를 생성하는 것보다 더 간결한 또 다른 방법을 제공한다.

1
2
3
4
5
6
7
8
#(* % 3)

(#(* % 3) 8)
; => 24

(map #(str "Hi, " %)
     ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

이와 같이 이상한 스타일로 익명 함수를 작성하는 스타일은 reader macros 라는 기능으로 가능하다.

7장에서 이에 대한 모든 것을 배우게 될 것이다.

지금 당장은 이러한 익명 함수를 사용하는 방법을 배우는 것이 좋다.

이 구문은 확실히 더 간결하지만, 약간 이상하다는 것을 알 수 있다.

1
2
3
4
5
;; Function call
(* 8 3)

;; Anonymous function
#(* % 3)

직관적으로 이해할 수 있다.

그리고 %를 통해서 함수에 전달된 인수를 나타낼 수 있다.

여러 인수를 사용하는 경우에는, %1, %2, %3와 같이 구분할 수 있다.

기본적으로 %%1와 동일하다.

1
2
(#(str %1 " and " %2) "cornbread" "butter beans")
; => "cornbread and butter beans"

rest parameter로 %&을 전달할 수도 있다.

1
2
(#(identity %&) 1 "blarg" :yip)
; => (1 "blarg" :yip)

이 경우 rest parameter에 항등 함수(Identity)를 적용했다.

Identity는 변경하지 않고, 주어진 인수를 반환한다.

간단한 익명 함수를 작성해야 하는 경우, 시각적으로 간결하기 때문에 이 스타일을 사용하는 것이 가장 좋다.

반면에 더 길고 복잡한 함수를 작성하는 경우 쉽게 읽을 수 없게 될 수 있다.

그런 경우 fn 을 사용하자.

Returning Functions

지금까지 함수가 다른 함수를 반환할 수 있음을 확인했다.

반환된 함수는 closures이며, 이는 함수가 생성될 때 범위에 있던 모든 변수에 액세스할 수 있음을 의미한다.

1
2
3
4
5
6
7
8
9
(defn inc-maker
  "Create a custom incrementor"
  [inc-by]
  #(+ % inc-by))

(def inc3 (inc-maker 3))

(inc3 7)
; => 10

여기서, inc-by는 범위 내에 있으므로, 반환된 함수가 inc-maker외부에서 사용되는 경우에도 반환된 함수가 액세스할 수 있다.

References

This post is licensed under CC BY 4.0 by the author.

[Solidity] modifier 와 _; 그리고 Condition-Orientated Programming

-