# 클로저 인 액션(Clojure In Action)

함수형 스타일을 사용해야 하는 몇 가지 이유에 대해 살펴봤으니 이제 이러한 아이디어를 실제로 클로저에 적용하는 방법을 살펴봅시다.

Now that we've discussed some of the reasons to start using the functional style, let's see how to apply these ideas in practice with Clojure.

# 핵심(The Core)

# 데이터 타입(Data Types)

클로저는 여러 가지 표준 데이터 유형을 제공하며, 대부분은 익숙하게 보일 것입니다:

Clojure provides a number of standard data types, most of which should look familiar:

  • 변수는 변경 가능한 저장 위치를 제공합니다. 스레드 단위로 바인딩 및 리바운드할 수 있습니다.

  • 부울은 참 또는 거짓 값을 가질 수 있으며, nil 값도 거짓으로 취급됩니다.

  • 숫자는 정수, 복소수, 부동 소수점, 분수일 수 있습니다.

  • 기호는 변수의 식별자로 사용됩니다.

  • 키워드는 자신을 참조하는 기호로 콜론으로 표시되며 맵에서 키로 사용되는 경우가 많습니다.

  • 문자열은 큰따옴표로 표시되며 여러 줄에 걸쳐 있을 수 있습니다.

  • 문자는 앞에 백슬래시로 표시됩니다.

  • 정규식은 해시 기호가 앞에 붙은 문자열입니다.

  • Vars provide mutable storage locations. These can be bound and rebound on a per-thread basis.

  • Booleans can have a value of true or false; nil values are also treated as false.

  • Numbers can be integers, doubles, floats, and fractions.

  • Symbols are used as identifiers for variables.

  • Keywords are symbols that reference themselves and are denoted by a colon; these are often used as keys in maps.

  • Strings are denoted by double quotes and can span multiple lines.

  • Characters are denoted by a preceding backslash.

  • Regular expressions are strings prefixed with a hash symbol.

데이터 유형 외에도 목록, 벡터, 맵, 집합과 같은 일반적인 컬렉션 유형에 대한 리터럴 표기법을 Clojure에서 제공합니다:

In addition to the data types, Clojure provides us with a literal notation for common collection types such as lists, vectors, maps, and sets:

  • 리스트(List): '(1 2 3)
  • 벡터(Vector): [1 2 3]
  • 맵(Map): {:foo "a" :bar "b"}
  • 집합(Set): #{"a" "b" "c"}

흥미롭게도 클로저 로직은 데이터 구조를 사용하여 작성됩니다. 데이터와 로직 모두에 동일한 구문을 사용하면 강력한 메타프로그래밍 기능을 사용할 수 있습니다. 다른 데이터 구조와 마찬가지로 Clojure 코드의 모든 부분을 조작할 수 있습니다. 이 기능을 사용하면 문제 도메인에서 반복되는 패턴에 대한 코드를 템플릿으로 쉽게 만들 수 있습니다. Clojure에서는 코드가 곧 데이터이고 데이터가 곧 코드입니다.

Interestingly, Clojure logic is written using its data structures. Using the same syntax for both data and logic allows for powerful metaprogramming features. We can manipulate any piece of Clojure code just like we would any other data structure. This feature makes it trivial to template the code for recurring patterns in your problem domain. In Clojure, code is data and data is code.

# 특별한 양식(Special Forms)

특수 형식은 핵심 구문을 정의하는 if 조건문과 같은 작은 기본 요소 집합을 제공합니다. 이는 다른 언어에서 볼 수 있는 예약 키워드와 유사합니다. 그러나 대부분의 언어와 달리 클로저는 최소한의 예약 구문을 사용하며 언어의 대부분은 표준 라이브러리의 함수와 매크로를 사용하여 구현됩니다.

Special forms provide a small set of primitives, such as the if conditional, that define the core syntax. These are akin to reserved keywords found in other languages. However, unlike most languages, Clojure uses a minimal amount of reserved syntax and majority of the language is implemented using functions and macros in the standard library.

# 함수(Functions)

클로저의 함수 호출은 다른 주류 언어와 동일하게 작동합니다. 가장 큰 차이점은 클로저 버전에서는 함수 이름이 부모 뒤에 온다는 점입니다.

Function calls in Clojure work the same as any mainstream languages. The main difference being that the function name comes after the paren in the Clojure version.

functionName(param1, param2)


(function-name param1 param2)

이 차이에는 아주 간단한 이유가 있습니다. 함수 호출은 단순히 함수 이름과 해당 매개변수가 포함된 목록입니다. 클로저에서 목록은 호출 가능한 표현식을 만들기 위해 예약된 특수한 유형의 데이터 구조입니다. 목록 데이터 구조를 만들려면 목록 함수를 호출해야 합니다:

There is a very simple reason for this difference. The function call is simply a list containing the function name and its parameters. In Clojure, a list is a special type of data structure reserved for creating callable expressions. To create a list data structure we'd have to call the list function:

(list 1 2 3)

# 익명 함수 Anonymous Functions

이름에서 알 수 있듯이 익명 함수는 단순히 이름에 바인딩되지 않은 함수입니다. 단일 인수를 받아 출력하는 다음 함수를 살펴보겠습니다:

As the name implies, anonymous functions are simply functions that aren't bound to a name. Let's take a look at the following function that accepts a single argument and prints it:

(fn [arg] (println arg))

함수는 fn 형식 뒤에 인수가 포함된 벡터와 본문을 사용하여 정의됩니다. 위 함수를 목록의 첫 번째 항목으로 설정하고 인수를 두 번째 항목으로 설정하여 호출할 수 있습니다:

The function is defined by using the fn form followed by the vector containing its argument and the body. We could call the above function by setting it as a first item in a list and its argument as the second:

((fn [arg] (println arg)) "hello")
=>"hello"

클로저는 # 표기법을 사용하여 익명 함수를 정의하기 위한 구문 설탕을 제공합니다. 이를 사용하면 함수를 다음과 같이 더 간결하게 재작성할 수 있습니다:

Clojure provides syntactic sugar for defining anonymous functions using the # notation. With it we can rewrite our function more concisely as follows:

#(println %)

여기서 % 기호는 이름이 지정되지 않은 인수를 나타냅니다. 여러 개의 인자 뒤에는 각각 아래와 같이 위치를 나타내는 숫자가 붙습니다:

Here, the % symbol indicates an unnamed argument. Multiple arguments would each be followed by a number indicating its position as seen below:

#(println %1 %2 %3)

이러한 유형의 함수는 명명된 함수를 정의할 필요가 없는 일회성 계산을 수행해야 할 때 유용합니다. 이들은 일반적으로 잠시 후에 보게 될 고차 함수와 함께 사용됩니다.

This type of function is useful when you need to perform a one-off computations that don't warrant defining a named function. They are commonly used in conjunction with the higher-order functions that we'll see in a moment.

# 이름있는 함수(Named Functions)

명명된 함수는 식별자로 사용되는 기호에 바인딩된 단순한 익명 함수입니다. 클로저는 전역 변수를 생성하는 데 사용되는 'def'라는 특수 형식을 제공합니다. 이 형식은 이름과 할당할 본문을 받습니다. 다음과 같이 def 형식을 사용하여 명명된 함수를 만들 수 있습니다:

Named functions are simply anonymous functions bound to a symbol used as an identifier. Clojure provides a special form called def that's used for creating global variables. It accepts a name and the body to be assigned to it. We can create a named function using the def form as follows:

(def double
  (fn [x] (* 2 x)))

이 작업은 매우 일반적인 작업이기 때문에 클로저는 이 작업을 대신 수행하는 defn이라는 형식을 제공합니다:

Since this is such a common operation, Clojure provides a form called defn that does it for us:

(defn square [x]
  (* x x))

defn 형식은 첫 번째 인수가 함수의 이름이라는 점을 제외하면 위에서 살펴본 fn 형식과 동일하게 동작합니다. 함수의 본문은 여러 표현식으로 구성될 수 있습니다:

The defn form behaves the same as the fn form we saw above, except that its first argument is the name of the function. The body of the function can consist of multiple expressions:

(defn bmi [height weight]
  (println "height:" height)
  (println "weight:" weight)
  (/ weight (* height height)))

여기서는 키와 몸무게 매개변수를 사용하여 BMI를 계산하는 함수를 정의합니다. 본문은 두 개의 인쇄 문과 몸무게를 키의 제곱으로 나누는 호출로 구성됩니다. 모든 표현식은 안쪽에서 바깥쪽으로 값을 평가합니다. 마지막 문에서 (* height height)가 평가된 다음 무게를 결과로 나누어 반환합니다. 클로저에서 /*와 같은 수학 연산자는 일반 함수이므로 다른 함수에서와 마찬가지로 접두사 표기법을 사용하여 호출합니다.

Here we define a function to calculate the BMI using the height and weight parameters. The body consists of two print statements and a call to divide the weight by the square of the height. All the expressions are evaluated from the inside out. In the last statement, (* height height) is evaluated, then the weight is divided by the result and returned. In Clojure, mathematical operators, such as / and *, are regular functions and so we call them using the prefix notation as we would with any other function.

마지막 표현식의 결과만 함수에서 반환되고 다른 모든 표현식의 결과는 버려집니다. 따라서 중간 표현식은 위의 println 호출의 경우처럼 부작용이 발생할 수 있으므로 엄격하게 사용해야 합니다.

Note that only the result from the last expression is returned from the function, the results of all the other expressions are discarded. Therefore, any intermediate expressions should strictly be used for side effects as is the case with the println calls above.

한 가지 주의해야 할 점은 클로저는 단일 패스 컴파일러를 사용한다는 것입니다. 따라서 함수를 사용하기 전에 반드시 선언해야 합니다. 함수가 선언되기 전에 함수를 참조해야 하는 경우, 정방향 선언을 제공하기 위해 declare 매크로를 사용해야 합니다.

One thing to note is that Clojure uses a single pass compiler. For this reason, the functions must be declared before they are used. In a case when we need to refer to a function before it's been declared, we must use the declare macro in order to provide a forward declaration.

(declare down)

(defn up [n]
  (if (< n 10)
    (down (+ 2 n)) n))

(defn down [n]
  (up (dec n)))

예리한 독자라면 코드가 트리 구조로 되어 있다는 것을 눈치챘을 것입니다. 이 트리를 추상 구문 트리 또는 줄여서 AST라고 합니다. AST를 직접 볼 수 있으면 논리 조각 간의 관계를 시각적으로 살펴볼 수 있습니다.

A keen reader will have noticed that the code is structured as a tree. This tree is called the abstract syntax tree, or AST for short. By being able to see the AST directly, we can examine the relationships between pieces of logic visually.

데이터로 코드를 작성하기 때문에 대부분의 언어에 비해 구문 힌트가 적습니다. 예를 들어, 명시적인 반환 문이 없고 함수 본문의 마지막 표현식이 암시적으로 반환됩니다.

Since we write our code in terms of data, there are fewer syntactic hints than in most languages. For example, there is no explicit return statement and the last expression of the function body is returned implicitly.

코드에서 많은 주석을 보는 데 익숙한 사용자라면 익숙해지는 데 시간이 조금 걸릴 수 있습니다. 가독성을 높이기 위해 함수는 종종 짧게 유지되고 들여쓰기와 간격은 시각적으로 코드를 그룹화하는 데 사용됩니다.

This might take a little getting used to if you're accustomed to seeing a lot of annotations in your code. To aid readability, functions are often kept short while indentation and spacing are used for grouping code visually.

클로저에서는 함수와 변수를 구분하지 않습니다. 레이블에 함수를 할당하거나, 매개변수로 전달하거나, 다른 함수에서 함수를 반환할 수 있습니다. 데이터로 취급할 수 있는 함수는 추가적인 제한이 없기 때문에 일류라고 합니다.

In Clojure, there is no distinction between functions and variables. You can assign a function to a label, pass it as a parameter, or return a function from another function. Functions that can be treated as data are referred to as being first-class because they don't have any additional restrictions attached to them.

# 고차 함수 (Higher-Order Functions)

다른 함수를 매개변수로 받는 함수를 고차 함수라고 합니다. 이러한 함수의 한 가지 예로 map이 있습니다:

Functions that take other functions as parameters are called higher-order functions. One example of such a function is map:

(map #(* % %) [1 2 3 4 5]) => (1 4 9 16 25)

이 함수는 두 개의 매개변수를 받는데, 첫 번째 매개변수는 인수를 제곱하는 익명 함수이고 두 번째 매개변수는 숫자 컬렉션입니다. 지도 함수는 컬렉션의 각 항목을 방문하여 제곱합니다.

This function accepts two parameters where the first is an anonymous function that squares its argument and the second is a collection of numbers. The map function will visit each item in the collection and square it.

고차 함수를 사용할 때 얻을 수 있는 가장 큰 장점은 사용되는 함수에서 코드의 의도를 유추할 수 있다는 것입니다. 위의 예제를 명령형 루프와 대조해 보겠습니다:

One major advantage of using a higher order function is that we can infer the intent of the code from the function being used. Let's contrast the above example to an imperative style loop:

(loop [[n & numbers] [1 2 3 4 5]
       result []]
  (let [result (conj result (* n n))]
    (if numbers
      (recur numbers result)
      result)))

루핑 접근 방식은 결국 더 많은 노이즈가 발생하므로 코드가 무엇을 하는지 파악하기 위해 코드를 더 주의 깊게 읽어야 합니다. 또 다른 문제는 코드가 모놀리식이 되어 어떤 부분도 개별적으로 사용할 수 없다는 것입니다.

The looping approach ends up having a lot more noise and thus we have to read through the code more carefully to tell what it's doing. The other problem is that the code becomes monolithic and no part of it can be used individually.

고차 함수의 또 다른 예는 '필터(filter)'입니다. 이 함수는 컬렉션을 통과하여 지정된 술어와 일치하는 항목만 유지합니다.

Another example of a higher-order function is filter. This function goes through a collection and keeps only the items matching the specified predicate.

(filter even? [1 2 3 4 5]) => (2 4)

고차 함수를 쉽게 연결하여 복잡한 변환을 만들 수 있습니다:

Higher order functions can be easily chained together to create complex transformations:

(filter even?
  (map #(* 3 %) [1 2 3 4 5]))

=>(6 12)

여기서는 각 항목에 3을 곱한 다음 'filter'를 사용하여 결과 시퀀스에서 짝수 항목만 유지합니다. 고차 함수를 사용하면 루프나 명시적 재귀를 작성할 필요가 거의 없습니다. 컬렉션을 반복할 때는 map이나 filter와 같은 함수를 대신 사용하세요. 클로저에는 풍부한 표준 라이브러리가 있기 때문에 라이브러리에 있는 함수를 조합하면 거의 모든 데이터 변환을 쉽게 수행할 수 있습니다. 여기 (opens new window)에서 이 접근 방식의 몇 가지 예시를 참조하세요.

Here we multiply each item by 3, then we use filter to only keep the even items from the resulting sequence. Having higher-order functions means that you should rarely have to write loops or explicit recursion. When iterating a collection, use a function such as map or filter instead. Since Clojure has a rich standard library, practically any data transformation can be easily achieved by combining functions found there. See here (opens new window) for some examples of this approach in action.

데이터 변환을 특정 함수와 연관시키는 방법을 배우면 이러한 함수를 특정 순서로 조합하는 것만으로도 많은 문제를 해결할 수 있습니다.

Once you learn to associate data transformations with specific functions, many problems can be solved by simply putting these functions together in a specific order.

이 아이디어를 간단한 실제 문제에 사용하는 방법을 살펴보겠습니다. 주소를 나타내는 필드가 지정된 형식의 주소를 표시하고 싶습니다. 일반적으로 주소에는 단위 번호, 거리, 도시, 우편 번호 및 국가가 있습니다. 이러한 각 필드를 검사하여 'nil'과 비어 있는 필드를 제거하고 그 사이에 구분 기호를 삽입해야 합니다. 다음 필드가 포함된 테이블이 주어집니다:

Let's take a look at using this idea for a simple real world problem. We'd like to display a formatted address given the fields representing it. Commonly an address has a unit number, a street, a city, a postal code, and a country. We'll have to examine each of these fields, remove the nil and empty ones, and insert a separator between them. Given a table containing the following fields:

unit      | street          | city      | postal_code | country
""        | "1 Main street" | "Toronto" | nil         | "Canada"

테이블의 문자열을 사용하여 다음과 같은 형식의 문자열을 출력하려고 합니다:

We would like to output the following formatted string using the strings in the table:

"1 Main street, Toronto, Canada"

빈 필드를 제거하고, 구분 기호를 삽입하고, 결과를 문자열로 연결하는 작업을 위한 함수를 찾기만 하면 됩니다:

All we have to do is find the functions for the tasks of removing empty fields, interposing the separator, and concatenating the result into a string:

(defn concat-fields [& fields]
  (clojure.string/join ", " (remove empty? fields)))

(concat-fields "" "1 Main street" "Toronto" nil "Canada")
=> "1 Main street, Toronto, Canada"

코드를 작성할 때 작업 수행 방법을 지정할 필요가 없다는 점에 주목하세요. 대부분의 경우 수행하고자 하는 작업을 나타내는 함수를 구성하여 수행하고자 하는 작업을 간단히 설명하기만 하면 됩니다. 결과 코드는 모든 일반적인 에지 케이스도 처리합니다:

Notice that we didn't have to specify how to do any of the tasks when writing our code. Most of the time we simply say what we're doing by composing the functions representing the operations we wish to carry out. The resulting code also handles all the common edge cases:

(concat-fields) => ""
(concat-fields nil) => ""
(concat-fields "") => ""

클로저에서는 코드가 모든 입력에 대해 즉시 올바르게 작동하는 것이 일반적입니다.

In Clojure, it's common for the code to work correctly for all inputs out of the box.

# 클로저(Closures)

이제 함수를 선언하고 이름을 지정하고 다른 함수에 매개변수로 전달하는 방법을 살펴봤습니다. 마지막으로 할 수 있는 것은 다른 함수를 결과로 반환하는 함수를 작성하는 것입니다. 이러한 함수의 한 가지 용도는 객체 지향 언어의 생성자가 제공하는 기능을 제공하는 것입니다.

We've now seen how we can declare functions, name them, and pass them as parameters to other functions. One last thing we can do is write functions that return other functions as their result. One use for such functions is to provide the functionality facilitated by constructors in object-oriented languages.

손님에게 따뜻한 인사말을 건네고 싶다고 가정해 봅시다. 인사말 문자열을 매개변수로 받아들이고 게스트의 이름을 받아 해당 게스트에 대한 사용자 지정 인사말을 인쇄하는 함수를 반환하는 함수를 작성할 수 있습니다:

Let's say we wish to greet our guests with a warm greeting. We can write a function that will accept the greeting string as its parameter and return a function that takes the name of the guest and prints a customized greeting for that guest:

(defn greeting [greeting-string]
  (fn [guest]
    (println greeting-string guest)))

(let [greet (greeting "Welcome to the wonderful world of Clojure")]
  (greet "Jane")
  (greet "John"))

greeting의 내부 함수는 값이 외부 범위에 정의되어 있으므로 greeting-string 값에 액세스할 수 있습니다. greeting 함수는 매개변수(여기서는 greeting-string)를 닫고 반환하는 함수에서 사용할 수 있도록 하기 때문에 클로저라고 합니다.

The inner function in the greeting has access to the greeting-string value since the value is defined in its outer scope. The greeting function is called a closure because it closes over its parameters, in our case the greeting-string, and makes them available to the function that it returns.

또한 let이라는 형식을 사용하여 greet 기호를 바인딩하고 그 안에 있는 모든 표현식에서 사용할 수 있도록 하고 있다는 것을 알 수 있습니다. let` 형식은 명령형 언어에서 변수를 선언하는 것과 같은 용도로 사용됩니다.

You'll also notice that we're using a form called let to bind the greet symbol and make it available to any expressions inside it. The let form serves the same purpose as declaring variables in imperative languages.

# 표현식을 연결시키기(Threading Expressions)

이쯤 되면 중첩된 표현식이 읽기 어려울 수 있다는 것을 눈치채셨을 것입니다. 다행히도 클로저는 이 문제를 해결하기 위한 몇 가지 도우미 형식을 제공합니다. 숫자 범위가 있고 각 숫자를 증가시키고 그 사이에 숫자 5를 삽입한 다음 결과를 합산하고 싶다고 가정해 보겠습니다. 이를 위해 다음 코드를 작성할 수 있습니다:

By this point you're probably noticing that nested expressions can get difficult to read. Fortunately, Clojure provides a couple of helper forms to deal with this problem. Let's say we have a range of numbers, and we want to increment each number, interpose the number 5 between them, then sum the result. We could write the following code to do that:

(reduce + (interpose 5 (map inc (range 10))))

위에서 무슨 일이 일어나고 있는지 한눈에 파악하기는 조금 어렵습니다. 체인에 단계가 몇 개 더 있으면 정말 길을 잃을 것입니다. 게다가 증분하기 전에 5를 끼워 넣는 등 단계를 재배열하려면 모든 표현식을 다시 작성해야 합니다. 위의 표현식을 작성하는 다른 방법은 ->> 형식을 사용하는 것입니다:

It's a little difficult to tell what's happening above at a glance. With a few more steps in the chain we'd be really lost. On top of that, if we wanted to rearrange any of the steps, such as interposing 5 before incrementing, then we'd have to renest all our expressions. An alternative way to write the above expression is to use the ->> form:

(->> (range 10) (map inc) (interpose 5) (reduce +))

여기서는 ->>를 사용하여 연산을 하나에서 다음 연산으로 스레딩합니다. 즉, 각 표현식의 결과를 다음 표현식의 마지막 인수로 암시적으로 전달합니다. 첫 번째 인수로 전달하려면 대신 -> 형식을 사용합니다.

Here, we use ->> to thread the operations from one to the next. This means that we implicitly pass the result of each expression as the last argument of the next expression. To pass it as the first argument we'd use the -> form instead.

# 게으름 (Laziness)

많은 클로저 알고리즘은 결과를 실제로 평가할 필요가 없는 한 연산을 수행하지 않는 지연 평가를 사용합니다. 지연은 많은 알고리즘을 효율적으로 작동시키는 데 매우 중요합니다. 예를 들어, 앞의 예는 범위를 만들고, 매핑하고, 숫자를 삽입하고, 결과를 줄이기 위해 매번 시퀀스를 반복해야 하므로 매우 비효율적이라고 생각할 수 있습니다.

Many Clojure algorithms use lazy evaluation where the operations aren't performed unless their result actually needs to be evaluated. Laziness is crucial for making many algorithms work efficiently. For example, you might think the preceding example is very inefficient since we have to iterate our sequence each time to create the range, map across it, interpose the numbers, and reduce the result.

하지만 실제로는 그렇지 않습니다. 각 표현식의 평가는 필요에 따라 이루어집니다. 범위의 첫 번째 값이 생성되어 나머지 함수에 전달되고, 다음 값이 전달되는 식으로 시퀀스가 소진될 때까지 반복됩니다. 이는 Python과 같은 언어가 반복자 메커니즘에서 취하는 접근 방식과 유사합니다.

However, this is not actually the case. The evaluation of each expression happens on demand. The first value in the range is generated and passed to the rest of the functions, then the next, and so on, until the sequence is exhausted. This is a similar approach that languages like Python take with their iterator mechanics.

# 코드 구조 (Code Structure)

클로저와 명령형 언어의 사소하지 않은 차이점 중 하나는 코드가 구조화되는 방식입니다. 명령형 스타일에서는 공유 가변 변수를 선언하고 다른 함수를 전달하여 변수를 수정하는 것이 일반적인 패턴입니다. 메모리 위치에 액세스할 때마다 이전에 해당 메모리 위치에서 작업한 코드의 결과를 볼 수 있습니다. 예를 들어 정수 목록이 있고 각 정수를 제곱한 다음 짝수만 인쇄하려는 경우 다음 Python 코드가 완벽하게 유효합니다:

One nontrivial difference between Clojure and imperative languages is the way the code is structured. In imperative style, it's a common pattern to declare a shared mutable variable and modify it by passing it different functions. Each time we access the memory location we see the result of the code that previously worked with it. For example, if we have a list of integers and we wish to square each one then print the even ones, the following Python code would be perfectly valid:

l = range(1, 6)

for i, val in enumerate(l) :
  l[i] = val * val

for i in l :
  if i % 2 == 0 :
    print i

클로저에서는 이러한 상호 작용이 명시적으로 이루어져야 합니다. 공유 메모리 위치를 생성한 다음 여러 함수가 순차적으로 액세스하는 대신, 함수를 서로 연결하고 입력을 파이프로 전달합

In Clojure this interaction has to be made explicit. Instead of creating a shared memory location and then having different functions access it sequentially, we chain functions together and pipe the input through them:

(println
  (filter #(= (mod % 2) 0)
    (map #(* % %) (range 1 6))))

앞서 소개한 ->> 매크로를 사용하여 단계를 평탄화할 수도 있습니다:

We could also flatten out the steps using the ->> macro introduced earlier:

(->> (range 1 6)
     (map #(* % %))
     (filter #(= (mod % 2) 0))
     (println))

각 함수는 기존 데이터를 수정하는 대신 새로운 값을 반환합니다. 이렇게 하면 비용이 많이 들 수 있으며, 변경할 때마다 데이터 전체를 복사하는 순진한 구현 방식이라고 생각할 수 있습니다.

Each function returns a new value instead of modifying the existing data in place. You might think that this can get very expensive, and it would be with a naïve implementation where the entirety of the data is copied with every change.

실제로 Clojure는 데이터의 인메모리 수정본을 생성하는 영구 데이터 구조로 뒷받침됩니다. 변경이 이루어질 때마다 변경 크기에 비례하여 새로운 수정본이 생성됩니다. 이 접근 방식을 사용하면 변경 사항이 로컬라이즈되도록 보장하면서 이전 구조와 새 구조 간의 차이에 대한 대가만 지불하면 됩니다.

In reality, Clojure is backed by persistent data structures that create in-memory revisions of the data. Each time a change is made a new revision is created proportional to the size of the change. With this approach we only pay the price of the difference between the old and the new structures while ensuring that any changes are localized.

# 구조 쪼개기(Destructuring)

클로저에는 데이터 구조의 값에 선언적으로 액세스하기 위한 강력한 메커니즘인 디스트럭처링이 있습니다. 이 기술은 데이터에 쉽게 액세스하고 함수에 대한 매개변수를 문서화하는 역할을 합니다. 몇 가지 예제를 통해 어떻게 작동하는지 살펴봅시다.

Clojure has a powerful mechanism called destructuring for declaratively accessing values in data structures. This technique provides easy access to the data and serves to document the parameters to a function. Let's look at some examples to see how it works.

(let [[smaller bigger] (split-with #(< % 5) (range 10))]
    (println smaller bigger))

=>(0 1 2 3 4) (5 6 7 8 9)

위에서는 split-with 함수를 사용하여 10개의 숫자 범위를 5보다 작은 숫자와 5보다 크거나 같은 숫자라는 두 가지 요소를 포함하는 시퀀스로 분할했습니다. 결과의 형식을 알고 있으므로 let 바인딩에서 [작은 더 큰]과 같이 리터럴 형식으로 작성할 수 있습니다. 디스트럭처링은 let 형식에 국한되지 않으며 함수 인자와 같은 모든 유형의 바인딩에 적용됩니다.

Above, we use split-with function to split a range of ten numbers into a sequence containing two elements: numbers less than 5 and numbers greater than or equal to 5. Since we know the format of the result, we can write it in a literal form as [smaller bigger] in the let binding. Destructuring is not limited to the let form and works for all types of bindings such as function arguments.

세 개의 요소가 있는 벡터를 받아 각각 name, address, phone에 바인딩하는 print-user라는 다른 함수를 살펴보겠습니다:

Let's look at another function called print-user that accepts a vector with three elements and binds them to nameaddress, and phone, respectively:

(defn print-user [[name address phone]]
  (println name "-" address phone))

(print-user ["John" "397 King street, Toronto" "416-936-3218"])
=> "John - 397 King street, Toronto 416-936-3218"

변수 인수를 시퀀스로 지정할 수도 있습니다. 이 경우 & 뒤에 나머지 인수가 포함된 목록의 이름을 사용하면 됩니다:

We can also specify variable arguments as a sequence. This is done by using & followed by the name of the list containing the remaining arguments:

(defn print-args [& args]
  (println args))

(print-args "a" "b" "c") => (a b c)

변수 인수는 시퀀스에 저장되므로 다른 인자와 마찬가지로 파괴할 수 있습니

Since the variable arguments are stored in a sequence, it can be destructured like any other:

(defn print-args [arg1 & [arg2]]
  (println
    (if arg2
      "got two arguments"
      "got one argument")))

(print-args "bar")
=>"got one argument"

(print-args "bar" "baz")
=>"got two arguments"

디스트럭처링은 맵에도 적용할 수 있습니다. 맵을 디스트럭처링할 때는 원래 맵의 키를 가리키는 로컬 바인딩의 이름을 제공하는 새 맵을 만듭니다:

Destructuring can also be applied to maps. When destructuring a map, we create a new map where we supply the names for the local bindings pointing to the keys from the original map:

(let [{foo :foo bar :bar} {:foo "foo" :bar "bar"}]
  (println foo bar))

중첩된 데이터 구조를 파괴하는 것도 가능합니다. 전달되는 데이터의 구조만 알고 있다면 간단히 작성할 수 있습니다:

It's also possible to destructure a nested data structure. As long as you know the structure of the data being passed in, you can simply write it out:

(let [{[a b c] :items id :id} {:id "foo" :items [1 2 3]}]
  (println id "->" a b c))
=> "foo -> 1 2 3"

마지막으로, 맵에서 키를 추출하는 것은 매우 일반적인 작업이므로 Clojure는 이 작업을 위한 구문 설탕을 제공합니다:

Finally, since extracting keys from maps is a very common operation, Clojure provides syntactic sugar for this task:

(defn login [{:keys [user pass]}]
 (and (= user "bob") (= pass "secret")))

(login {:user "bob" :pass "secret"})

또 다른 유용한 디스트럭처링 옵션을 사용하면 원본 맵을 보존하면서 일부 키를 추출할 수 있습니다:

Another useful destructuring option allows us to extract some keys while preserving the original map:

(defn register [{:keys [id pass repeat-pass] :as user}]
  (cond
    (nil? id) "user id is required"
    (not= pass repeat-pass) "re-entered password doesn't match"
    :else user))

# 네임스페이스(Namespaces)

실제 애플리케이션을 작성할 때는 코드를 별도의 구성 요소로 구성할 수 있는 도구가 필요합니다. 객체 지향 언어는 이를 위해 클래스를 제공합니다. 관련 메서드는 모두 같은 클래스에 정의됩니다. 클로저에서는 함수를 네임스페이스로 그룹화합니다. 네임스페이스가 어떻게 정의되는지 살펴봅시다.

When writing real-world applications we need tools to organize our code into separate components. Object-oriented languages provide classes for this purpose. The related methods will all be defined in the same class. In Clojure, we group our functions into namespaces instead. Let's look at how a namespace is defined.

(ns colors)

(defn hex->rgb [[_ & rgb]]
    (map #(->> % (apply str "0x") (Long/decode))
         (partition 2 rgb)))

(defn hex-str [n]
  (-> (format "%2s" (Integer/toString n 16))
      (clojure.string/replace " " "0")))

(defn rgb->hex [color]
  (apply str "#" (map hex-str color)))

위에는 hex->rgb, hex-str, rgb->hex라는 세 가지 함수가 포함된 colors라는 네임스페이스가 있습니다. 같은 네임스페이스에 있는 함수는 서로를 직접 호출할 수 있습니다. 그러나 다른 네임스페이스에서 이러한 함수를 호출하려면 먼저 colors 네임스페이스를 참조해야 합니다.

Above, we have a namespace called colors containing three functions called hex->rgbhex-str, and rgb->hex. The functions in the same namespace can call each other directly. However, if we wanted to call these functions from a different namespace we would have to reference the colors namespace there first.

클로저는 이를 위해 두 가지 방법을 제공하는데, :use 또는 :require 키워드를 사용할 수 있습니다. 사용`으로 네임스페이스를 참조하면 해당 네임스페이스를 참조하는 네임스페이스에 정의된 것처럼 모든 변수를 암시적으로 사용할 수 있게 됩니다.

Clojure provides two ways to do this, we can either use the :use or the :require keywords. When we reference a namespace with :use, all its Vars become implicitly available as if they were defined in the namespace that references it.

(ns myns
  (:use colors))

(hex->rgb "#33d24f")

이 접근 방식에는 두 가지 단점이 있습니다. 함수가 원래 어디에 정의되었는지 알 수 없어 코드를 탐색하기 어렵고, 함수에 동일한 이름을 사용하는 두 개의 네임스페이스를 참조하면 오류가 발생합니다.

There are two downsides to this approach. We don't know where the function was originally defined, making it difficult to navigate the code, and if we reference two namespaces that use the same name for a function, we'll get an error.

첫 번째 문제는 :use 선언에서 :only 키워드를 사용하여 명시적으로 사용하려는 함수를 선택함으로써 해결할 수 있습니다.

We can address the first problem by selecting the functions we wish to use explicitly using the :only keyword in our :use declaration.

(ns myns
  (:use [colors :only [rgb->hex]]))

(defn hex-str [c]
  (println "I don't do much yet"))

이렇게 하면 rgb->hex의 출처를 문서화할 수 있고, myns 네임스페이스에서 충돌 없이 자체 hex-str 함수를 선언할 수 있습니다. rgb->hex는 여전히colors네임스페이스에 정의된hex-str` 함수를 사용한다는 점에 유의하세요.

This way we document where rgb->hex comes from, and we're able to declare our own hex-str function in the myns namespace without conflicts. Note that rgb->hex will still use the hex-str function defined in the colors namespace.

네임스페이스를 참조하기 위해 :require 키워드를 사용하는 접근 방식은 보다 유연한 옵션을 제공합니다. 각각을 살펴보겠습니다.

The approach of using the :require keyword to reference the namespace provides us with more flexible options. Let's look at each of these.

추가 지시어를 제공하지 않고 네임스페이스를 요구할 수 있습니다. 이 경우, 네임스페이스 내부의 Vars에 대한 모든 호출은 그 출처를 나타내는 네임스페이스 선언을 앞에 붙여야 합니다.

We can require a namespace without providing any further directives. In this case, any calls to Vars inside it must be prefixed with the namespace declaration indicating their origin.

(ns myns (:require colors))

(colors/hex->rgb "#324a9b")

이 접근 방식은 참조되는 Vars의 출처를 명시하고 여러 네임스페이스를 참조할 때 충돌이 발생하지 않도록 보장합니다. 한 가지 문제점은 네임스페이스 선언이 길면 그 안에 선언된 함수를 사용하고자 할 때마다 입력해야 하는 번거로움이 있다는 것입니다. 이 문제를 해결하기 위해 :require 문은 :as 지시문을 제공하여 네임스페이스의 별칭을 생성할 수 있도록 합니다.

This approach is explicit about the origin of the Vars being referenced and ensures that we won't have conflicts when referencing multiple namespaces. One problem is that when our namespace declaration is long, it gets tedious to have to type it out any time we wish to use a function declared inside it. To address this problem, the :require statement provides the :as directive, allowing us to create an alias for the namespace.

(ns myotherns
  (:require [colors :as c]))

(c/hex->rgb "#324a9b")

참조키워드를 사용하여 네임스페이스에서 함수를 요구할 수도 있습니다. 이는 앞서 살펴본:use` 표기법과 동의어입니다. 다른 네임스페이스의 모든 함수를 요구하려면 다음과 같이 작성하면 됩니다:

We can also require functions from a namespace by using the :refer keyword. This is synonymous with the :use notation we saw earlier. To require all the functions from another namespace, we can write the following:

(ns myns
  (:require [colors :refer :all]))

이름별로 필요한 함수를 선택하려면 대신 다음과 같이 작성하면 됩니다:

If we wish to select what functions to require by name, we can instead write:

(ns myns
  (:require [colors :refer [rgb->hex]))

보시다시피 다른 네임스페이스에 선언된 Vars를 참조하는 데 사용할 수 있는 옵션은 여러 가지가 있습니다. 어떤 옵션을 선택해야 할지 잘 모르겠다면 이름 또는 별칭으로 네임스페이스를 요청하는 것이 가장 안전한 방법입니다.

As you can see, there's a number of options available for referencing Vars declared in other namespaces. If you're not sure what option to pick, then requiring the namespace by name or alias is the safest route.

# 동적 변수 Dynamic Variables

클로저는 특정 범위 내에서 값을 변경할 수 있는 동적 변수를 선언하는 기능을 지원합니다. 이것이 어떻게 작동하는지 살펴봅시다.

Clojure provides support for declaring dynamic variables that can have their value changed within a particular scope. Let's look at how this works.

(declare ^{:dynamic true} *foo*)

(println *foo*)
=>#<Unbound Unbound: #'bar/*foo*>

여기서는 *foo*를 동적 변수로 선언하고 값을 제공하지 않았습니다. *foo*를 출력하려고 하면 이 변수가 어떤 값에도 바인딩되지 않았음을 나타내는 오류가 발생합니다. 바인딩을 사용하여 *foo*에 값을 할당하는 방법을 살펴봅시다.

Here we declared *foo* as a dynamic Var and didn't provide any value for it. When we try to print *foo* we get an error indicating that this Var has not been bound to any value. Let's look at how we can assign a value to *foo* using a binding.

(defn with-foo [f]
  (binding [*foo* "I exist!"]
    (f)))

(with-foo #(println *foo*)) =>"I exist!"

with-foo 함수 내에서 *foo*를 "I exist!"라는 값을 가진 문자열로 설정합니다. 익명 함수가 with-foo 안에서 호출되면 값을 출력하려고 할 때 더 이상 오류가 발생하지 않습니다.

We set *foo* to a string with value "I exist!" inside the with-foo function. When our anonymous function is called inside with-foo we no longer get an error when trying to print its value.

이 기법은 파일 스트림, 데이터베이스 연결 또는 범위가 지정된 변수와 같은 리소스를 다룰 때 유용할 수 있습니다. 일반적으로 동적 변수는 코드를 더 불투명하게 만들고 추론하기 어렵게 만들기 때문에 사용하지 않는 것이 좋습니다. 하지만 동적 변수를 합법적으로 사용할 수 있는 경우도 있으므로 작동 원리를 알아두는 것이 좋습니다.

This technique can be useful when dealing with resources such as file streams, database connections, or scoped variables. In general, the use of dynamic variables is discouraged since they make code more opaque and difficult to reason about. However, there are legitimate uses for them, and it's worth knowing how they work.

# 폴리모피즘(Polymorphism)

객체 지향의 유용한 측면 중 하나는 다형성이지만, 다형성이 해당 스타일과 연관되어 있기는 하지만 결코 배타적이지 않습니다. 클로저는 런타임 다형성을 달성하는 두 가지 일반적인 방법을 제공합니다. 각각을 차례로 살펴보겠습니다.

One useful aspect of object-orientation is polymorphism, while it happens to be associated with that style it's in no way exclusive to it. Clojure provides two common ways to achieve runtime polymorphism. Let's look at each of these in turn.

# 멀티메소드(Multimethods)

다중 메서드는 하나 이상의 메서드와 연결된 선택자 함수를 사용하여 매우 유연한 디스패치 메커니즘을 제공합니다. 다중 메서드는 defmulti를 사용하여 정의되며, 그 메서드는 각각 defmethod를 사용하여 정의됩니다. 예를 들어 서로 다른 도형이 있고 면적을 계산하는 다중 메서드를 작성하고자 한다면 다음과 같이 할 수 있습니다:

Multimethods provide an extremely flexible dispatching mechanism using a selector function associated with one or more methods. The multimethod is defined using defmulti and its methods are each defined using defmethod. For example, if we had different shapes and we wanted to write a multimethod to calculate the area we could do the following:

(defmulti area :shape)

(defmethod area :circle [{:keys [r]}]
  (* Math/PI r r))

(defmethod area :rectangle [{:keys [l w]}]
  (* l w))

(defmethod area :default [shape]
  (throw (Exception. (str "unrecognized shape: " shape))))

(area {:shape :circle :r 10})
=> 314.1592653589793

(area {:shape :rectangle :l 5 :w 10})
=> 50

위의 dispatch 함수는 키워드를 사용하여 각 맵 유형을 처리하는 데 적합한 방법을 선택합니다. 키워드가 함수처럼 작동하고 지도가 전달되면 키워드와 연관된 값을 반환하기 때문에 이 함수가 작동합니다. 하지만 디스패치 함수는 원하는 만큼 정교하게 만들 수 있습니다:

Above, the dispatch function uses a keyword to select the appropriate method to handle each type of map. This works because keywords act as functions and when passed a map will return the value associated with them. The dispatch function can be as sophisticated as we like however:

(defmulti encounter
  (fn [x y] [(:role x) (:role y)]))

(defmethod encounter [:manager :boss] [x y]
  :promise-unrealistic-deadlines)

(defmethod encounter [:manager :developer] [x y]
  :demand-overtime)

(defmethod encounter [:developer :developer] [x y]
  :complain-about-poor-management)

(encounter {:role :manager} {:role :boss})
=> :promise-unrealistic-deadlines

# 프로토콜(Protocols)

프로토콜을 사용하면 구체적인 유형으로 구현할 수 있는 추상적인 함수 집합을 정의할 수 있습니다. 프로토콜의 예를 살펴보겠습니다:

Protocols allow defining an abstract set of functions that can be implemented by a concrete type. Let's look at an example protocol:

(defprotocol Foo
  "Foo doc string"
  (bar [this b] "bar doc string")
  (baz [this] [this b] "baz doc string"))

보시다시피, Foo 프로토콜은 barbaz라는 두 가지 메서드를 지정합니다. 메서드의 첫 번째 인자는 타입 인스턴스이고 그 뒤에 매개변수가 있습니다. 바즈` 메서드에는 여러 개의 어리티가 있다는 점에 유의하세요. 이제 deftype 매크로를 사용해 Foo 프로토콜을 구현하는 타입을 만들 수 있습니다:

As you can see, the Foo protocol specifies two methods, bar and baz. The first argument to the method will be the type instance followed by its parameters. Note that the baz method has multiple arity. We can now create a type that implements the Foo protocol using the deftype macro:

(deftype Bar [data] Foo
  (bar [this param]
    (println data param))
  (baz [this]
    (println (class this)))
  (baz [this param]
    (println param)))

여기서는 프로토콜 Foo를 구현하는 Bar 타입을 생성합니다. 이 타입의 각 메서드는 전달된 매개변수 중 일부를 출력합니다. Bar`의 인스턴스를 생성하고 그 메서드를 호출하면 어떤 모습인지 살펴봅시다:

Here we create type Bar that implements protocol Foo. Each of its methods will print out some of the parameters passed to it. Let's see what it looks like when we create an instance of Bar and call its methods:

(let [b (Bar. "some data")]
  (.bar b "param")
  (.baz b)
  (.baz b "baz with param"))


some data param
Bar
baz with param

첫 번째 메서드 호출은 Bar가 초기화된 데이터와 전달된 파라미터를 출력합니다. 두 번째 메서드 호출은 객체의 클래스를 출력하고, 마지막 메서드 호출은 baz의 다른 기능을 보여줍니다.

The first method call prints out the data Bar was initialized with and the parameter that was passed in. The second method call prints out the object's class, while the last method call demonstrates the other arity of baz.

프로토콜을 사용하여 기존 Java 클래스를 포함한 기존 유형의 기능을 확장할 수도 있습니다. 예를 들어 extend-protocol을 사용하여 java.lang.String 클래스를 Foo 프로토콜로 확장할 수 있습니다:

We can also use protocols to extend the functionality of existing types, including existing Java classes. For example, we can use extend-protocol to extend the java.lang.String class with the Foo protocol:

(extend-protocol Foo String
  (bar [this param] (println this param)))

(bar "hello" "world")
=>"hello world"

위의 예시는 프로토콜을 사용해 다형성 코드를 작성하는 방법의 기본 원칙을 보여줍니다. 하지만 프로토콜은 다른 용도로도 많이 사용되므로 직접 찾아보시기를 권장합니다.

The above examples illustrate the basic principles of how protocols can be used to write polymorphic code. However, there are many other uses for protocols as well and I encourage you to discover these on your own.

# 글로벌 상태 처리(Dealing With Global State)

주로 불변성이지만, 클로저는 표준 라이브러리의 STM 함수를 통해 공유 가변 데이터에 대한 지원을 제공합니다. STM은 공유 가변 변수에 대한 모든 업데이트가 원자 단위로 이루어지도록 하는 데 사용됩니다.

While predominantly immutable, Clojure provides support for shared mutable data via its STM functions in the standard library. The STM is used to ensure that all updates to shared mutable variables are done atomically.

가변 변수는 크게 두 가지 유형이 있습니다: atomref. atom(원자)는 조정되지 않은 업데이트를 수행해야 하는 경우에 사용되며, 참조는 트랜잭션으로 여러 번 업데이트해야 할 때 사용됩니다. atom를 정의하고 사용하는 예를 살펴봅시다.

There are two primary mutable types: the atom and the ref. The atom is used in cases where we need to do uncoordinated updates and the ref is used when we might need to do multiple updates as a transaction. Let's look at an example of defining an atom and using it.

(def global-val (atom nil))

위에서 'global-val'이라는 'atom'(원자)를 만들었고 현재 값은 'nil'입니다. 이제 현재 값을 반환하는 deref 함수를 사용하여 그 값을 읽을 수 있습니다.

Above, we created an atom called global-val and its current value is nil. We can now read its value by using the deref function, which returns the current value.

(println (deref global-val)) => nil

이 작업은 일반적인 작업이므로 deref의 약어인 @ 기호가 있습니다:

Since this is a common operation, there is a shorthand for deref: the @ symbol:

(println @global-val)

위의 코드는 앞의 예제와 동일합니다.

The above code is equivalent to the preceding example.

원자에 새 값을 설정하는 두 가지 방법을 살펴봅시다. reset!을 사용해 새 값을 전달하거나, swap!을 사용해 현재 값을 업데이트하는 데 사용할 함수를 전달할 수 있습니다.

Let's look at two ways of setting a new value for our atom. We can either use reset! and pass in the new value, or we can use swap! and pass in a function that will be used to update the current value.

(reset! global-val 10) (println @global-val) =>10

(swap! global-val inc) (println @global-val) =>11

swap!reset! 모두 느낌표 !로 끝나는데, 이는 이 함수가 변경 가능한 데이터를 수정한다는 것을 나타내기 위한 규칙입니다.

Note that both swap! and reset! end in an exclamation point !; this is a convention to indicate that these functions modify mutable data.

참조를 정의하는 방식은 원자를 정의하는 방식과 같지만, 이 둘은 다소 다르게 사용됩니다. 아래에서 어떻게 작동하는지 간단히 살펴보겠습니다.

We define refs the same way we define atoms, but the two are used rather differently. Let's take a quick look at how they work below.

(def names (ref []))

(dosync
  (ref-set names ["John"])
  (alter names #(if (not-empty %)
                  (conj % "Jane") %)))

이 코드에서는 names라는 ref를 정의한 다음 dosync 문을 사용하여 트랜잭션을 엽니다. 트랜잭션 내에서 names"John" 값을 가진 벡터로 설정합니다. 다음으로 alter를 호출하여 names가 비어 있지 않은지 확인하고, 비어 있으면 "Jane"을 이름 벡터에 추가합니다.

In this code, we define a ref called names, then open a transaction using the dosync statement. Inside the transaction we set names to a vector with the value "John". Next, we call alter to check if names is not empty and add "Jane" to the vector of the names if that's the case.

이 작업은 트랜잭션 내부에서 이루어지므로 비어 있는지 확인하는 것은 기존 상태와 동일한 트랜잭션 내에 구축된 모든 상태에 따라 달라진다는 점에 유의하세요. 다른 트랜잭션에서 이름을 추가하거나 제거하려고 하면 우리 트랜잭션에는 아무런 영향을 미치지 않습니다. 충돌이 발생하면 트랜잭션 중 하나가 다시 시도됩니다.

Note that since this is happening inside a transaction, the check for emptiness depends on the existing state along with any state built up within the same transaction. If we tried to add or remove a name in a different transaction, it would have no visible effect on ours. In case of a collision, one of the transactions would end up being retried.

# 코드를 작성하는 코드 작성(Writing Code That Writes Code)

Lisp와 마찬가지로 Clojure는 강력한 매크로 시스템을 제공합니다. 매크로를 사용하면 반복적인 코드 블록을 템플릿화하고 평가를 연기하는 등 다양한 용도로 사용할 수 있습니다. 매크로는 코드를 평가하는 대신 데이터로 취급하는 방식으로 작동합니다. 따라서 다른 데이터 구조와 마찬가지로 코드 트리를 조작할 수 있습니다.

Clojure, being a Lisp, provides a powerful macro system. Macros allow templating repetitive blocks of code and deferring evaluation, among numerous other uses. A macro works by treating code as data instead of evaluating it. This allows us to manipulate the code tree just like any other data structure.

매크로는 평가 시간 전에 실행되며 평가자는 매크로 실행 결과를 볼 수 있습니다. 이러한 수준의 간접성 때문에 매크로는 추론하기 어려울 수 있으므로 함수가 작업을 수행할 때는 매크로를 사용하지 않는 것이 가장 좋습니다.

Macros execute before evaluation time and the evaluator sees the result of macro execution. Because of this level of indirection, macros can be difficult to reason about, and thus it's best not to use them when a function will do the job.

매크로의 구체적인 예를 통해 앞서 살펴본 일반 코드와 매크로가 어떻게 다른지 살펴보겠습니다. 사용자를 포함할 수 있는 세션 아톰이 있는 웹 애플리케이션이 있다고 가정해 보겠습니다. 사용자가 세션에 있을 때만 특정 콘텐츠를 로드하고 그렇지 않은 경우에는 로드하지 않으려 할 수 있습니다.

Let's look at a concrete example of a macro and see how it differs from the regular code we saw previously. Imagine that we have a web application with a session atom that might contain a user. We might want to load certain content only if a user is present in the session and not otherwise.

(def session (atom {:user "Bob"}))

(defn load-content []
  (if (:user @session)
    "Welcome back!"
    "please log in"))

이 방법은 작동하지만 매번 if 문을 작성하는 것은 지루하고 오류가 발생하기 쉽습니다. 조건의 로직은 동일하게 유지되므로 이 함수를 다음과 같이 템플릿화할 수 있습니다:

This will work, but it's tedious and error-prone to write out the if statement every single time. Since our condition's logic stays the same, we can template this function as follows:

(defmacro defprivate [name args & body]
  `(defn ~(symbol name) ~args
     (if (:user @session)
       (do ~@body)
       "please log in")))

매크로는 defmacro 특수 형식을 사용하여 정의됩니다. defndefmacro의 가장 큰 차이점은 defmacro에 전달된 매개변수는 기본적으로 평가되지 않는다는 것입니다.

The macros are defined using the defmacro special form. The major difference between defn and defmacro is that the parameters passed to defmacro are not evaluated by default.

매개변수를 평가하려면 ~(기호 이름)에서와 같이 ~를 사용합니다. '~` 표기법을 사용하면 이름이 참조하는 값으로 이름을 바꾸고 싶다는 것을 나타냅니다. 이를 인용 취소라고 합니다.

To evaluate the parameter we use the ~, as we're doing with ~(symbol name). Using the ~ notation indicates that we'd like to replace the name with the value it refers to. This is called unquoting.

(do ~@body)에서 사용되는 ~@ 표기법을 따옴표 제거라고 합니다. 이 표기법은 시퀀스를 다룰 때 사용됩니다. 시퀀스의 내용은 접합하는 동안 외부 형태로 병합됩니다. 이 경우 본문은 함수의 본문을 나타내는 목록으로 구성됩니다. if 문은 인수가 3개 이하여야 하므로 본문은 do 블록으로 감싸야 합니다.

The ~@ notation used in (do ~@body) is called unquote splicing. This notation is used when we're dealing with a sequence. The contents of the sequence will be merged into the outer form during the splicing. In this case the body consists of a list representing the function's body. The body must be wrapped in a do block because the if statement requires having no more than three arguments.

` 기호는 다음 목록을 실행하는 대신 데이터로 취급한다는 의미입니다. 이것은 인용 해제와 반대되는 것으로 구문 인용이라고 합니다.

The ` sign means that we wish to treat the following list as data instead of executing it. This is the opposite of unquoting, and it's referred to as syntax-quoting.

앞서 언급했듯이 매크로는 평가 시간 전에 실행됩니다. 평가자가 매크로를 볼 때 매크로가 어떻게 재작성되는지 확인하려면 macroexpand-1을 호출하면 됩니다.

앞서 언급했듯이 매크로는 평가 시간 전에 실행됩니다. 평가자가 매크로를 볼 때 매크로가 어떻게 재작성되는지 확인하려면 macroexpand-1을 호출하면 됩니다.

As I mentioned earlier, the macros are executed before evaluation time. To see what the macro will be rewritten as when the evaluator sees it, we can call macroexpand-1.

(macroexpand-1 '(defprivate foo [greeting] (println greeting)))

(clojure.core/defn foo [greeting]
  (if (:user (clojure.core/deref session))
    (do (println greeting))
    "please log in"))

(defprivate foo (println "bar"))가 내부에 if 문이 포함된 함수 정의로 재작성되는 것을 볼 수 있습니다. 이 결과 코드는 평가자에게 표시되는 코드이며, 그렇지 않으면 우리가 직접 작성해야 하는 코드와 동일합니다. 이제 매크로를 사용하여 비공개 함수를 간단히 정의할 수 있으며, 매크로가 자동으로 검사를 수행합니다.

We can see that (defprivate foo (println "bar")) gets rewritten with a function definition that has the if statement inside. This resulting code is what the evaluator will see, and it's equivalent to what we would have to write by hand otherwise. Now we can simply define a private function using our macro, and it will do the check for us automatically.

(defprivate foo [message] (println message))

(foo "this message is private")

앞의 예는 다소 인위적으로 보일 수 있지만, 코드에서 반복을 쉽게 템플릿화할 수 있다는 것이 얼마나 강력한지 보여줍니다. 이를 통해 문제 도메인을 자연스러운 언어를 사용하여 표현하는 표기법을 만들 수 있습니다.

The preceding example might seem a little contrived, but it demonstrates the power of being able to easily template repetitions in code. This allows creating a notation that expresses your problem domain using the language that is natural to it.

# 읽기-평가-출력 루프(The Read-Evaluate-Print Loop)

Clojure에서 작업할 때 가장 중요한 또 다른 측면은 읽기-평가-출력 루프(REPL)입니다. 많은 언어에서는 코드를 작성한 다음 전체 프로그램을 실행하여 어떤 동작을 하는지 확인합니다. Clojure에서는 대부분의 개발이 REPL을 사용하여 대화형으로 이루어집니다. 이 모드에서는 작성한 각 코드가 작성되는 즉시 작동하는 것을 볼 수 있습니다.

Another big aspect of working in Clojure is the read-evaluate-print loop (REPL). In many languages you write the code, then run the entire program to see what it does. In Clojure, most development is done interactively using the REPL. In this mode we can see each piece of code we write in action as soon as it's written.

중요하지 않은 애플리케이션에서는 더 많은 기능을 추가하기 전에 특정 상태를 구축해야 하는 경우가 많습니다. 예를 들어 사용자가 로그인하여 데이터베이스에서 일부 데이터를 쿼리한 다음 이 데이터의 형식을 지정하고 표시하는 함수를 작성해야 하는 경우입니다. REPL을 사용하면 애플리케이션을 변경할 때마다 애플리케이션을 다시 로드하고 상태를 빌드할 필요 없이 데이터가 로드되는 상태로 애플리케이션을 가져온 다음 대화형으로 표시 로직을 작성할 수 있습니다.

In nontrivial applications it's often necessary to build up a particular state before you can add more functionality. For example, a user has to log in and query some data from the database, then you need to write functions to format and display this data. With a REPL you can get the application to the state where the data is loaded and then write the display logic interactively without having to reload the application and build up the state every time you make a change.

이 개발 방법은 변경 시 즉각적인 피드백을 확인할 수 있어 특히 만족도가 높습니다. 쉽게 시도해보고 해결하려는 문제에 어떤 접근 방식이 가장 적합한지 확인할 수 있습니다. 이를 통해 실험을 계속하고 코드를 리팩터링할 수 있으므로 더 깔끔한 코드를 작성하는 데 도움이 됩니다.

This method of development is particularly satisfying because you see immediate feedback when making changes. You can easily try things out and see what approach works best for the problem you're solving. This encourages experimentation and refactoring code as you go, which in turn helps you to write better and cleaner code.

# Java로 호출하기 (Calling Out to Java)

One last thing that we'll cover is how Clojure embraces its host platform to benefit from the rich ecosystem of existing Java libraries. In some cases we may wish to call a Java library to accomplish a particular task that doesn't have a native Clojure implementation. Calling Java classes is very simple, and follows the standard Clojure syntax fairly closely.

# 클래스 가져오기 (Importing Classes)

마지막으로 다룰 마지막 내용은 기존 Java 라이브러리의 풍부한 에코시스템의 이점을 활용하기 위해 Clojure가 호스트 플랫폼을 수용하는 방법입니다. 어떤 경우에는 네이티브 Clojure 구현이 없는 특정 작업을 수행하기 위해 Java 라이브러리를 호출해야 할 수도 있습니다. Java 클래스를 호출하는 것은 매우 간단하며 표준 Clojure 구문을 매우 가깝게 따릅니다.

When we wish to use a Clojure namespace, we employ either the :use or the :require statements discussed above. However, when we wish to import a Java class, we have to use the :import statement instead:

(ns myns
  (:import java.io.File))

또한 다음과 같이 한 번의 임포트에서 동일한 패키지의 여러 클래스를 그룹화할 수도 있습니다:

We can also group multiple classes from the same package in a single import, as follows:

(ns myns
  (:import [java.io File FileInputStream FileOutputStream]))

# 클래스 인스턴스화(Instantiating Classes)

클래스의 인스턴스를 생성하려면 Java에서와 마찬가지로 new를 호출하면 됩니다:

To create an instance of a class, we can call new just as we would in Java:

(new File ".")

객체를 인스턴스화할 때 일반적으로 사용되는 속기도 있습니다:

There is also a commonly used shorthand for instantiating objects:

(File. ".")

# 메소드 호출하기(Calling Methods)

클래스의 인스턴스가 생기면 메서드 호출을 시작할 수 있습니다. 표기법은 일반 함수 호출과 비슷합니다. 메서드를 호출할 때 첫 번째 매개변수로 객체를 전달한 다음 메서드가 허용하는 다른 매개변수를 전달합니다.

Once we have an instance of a class, we can start calling methods on it. The notation is similar to making a regular function call. When we call a method, we pass the object as its first parameter followed by any other parameters that the method accepts.

(let [f (File. ".")]
  (println (.getAbsolutePath f)))

위에서는 새 파일 객체 f를 생성한 다음 그 위에 .getAbsolutePath 메서드를 호출했습니다. 메서드 앞에 마침표 .가 붙어서 일반 클로저 함수와 구분되는 것을 알 수 있습니다. 클래스의 정적 메서드나 변수를 참조하려면 대신 / 표기법을 사용합니다:

Above, we created a new file object f, and then called the .getAbsolutePath method on it. Notice that methods have a period . in front of them to differentiate them from a regular Clojure function. If we wanted to reference a static method or a variable in a class, we would use the / notation instead:

(str File/separator "foo" File/separator "bar")

(Math/sqrt 256)

'..` 표기법을 사용하여 여러 메서드 호출을 함께 연결하는 속기법도 있습니다. 파일 경로를 나타내는 문자열을 가져온 다음 해당 바이트를 가져오고 싶다고 가정하면, 두 가지 방법으로 코드를 작성할 수 있습니다.

There's also a shorthand for chaining multiple method calls together using the .. notation. Say we wanted to get the string indicating the file path and then get its bytes; we could write the code for that in two ways.

(.getBytes (.getAbsolutePath (File. ".")))

(.. (File. ".") getAbsolutePath getBytes)

# 추가 읽기 (Further Reading)

이것으로 클로저 기초에 대한 설명을 마쳤습니다. 전체 언어의 일부분만 다루었지만, 이 가이드를 통해 관용적인 Clojure 코드가 어떻게 작성되는지에 대한 약간의 통찰력을 얻으셨기를 바랍니다. 다음은 언어에 대한 보다 심층적인 문서에 대한 몇 가지 유용한 링크입니다.

This concludes our tour of Clojure basics. While we only touched on only a small portion of the overall language, I hope that the guide has provided you with a bit of insight into how idiomatic Clojure code is written. Below are some useful links for more in-depth documentation about the language.

Copyright © 2023 Dmitri Sotnikov

번역: damulhan@gmail.com, 파파고 등