User Tools

Site Tools


lecture:clojure:why_clojure

왜 클로져인가?


컴퓨팅 환경의 변화

멀티코어의 시대의 도래

무어의 법칙에 따르면 컴퓨터의 성능은 18개월마다 2배씩 증가한다. 기존까지는 이것이 하나의 칩에 더 많은 트랜지스터를 집적하고 클럭 속도를 증가시킴으로서 가능했다. 하지만 마이크로프로세서 제작의 기술적 한계1)에 의해 속도 증가는 불가능하게 되었다. 이제 무어의 법칙은 다른 방식으로 성립되는데 최근에는 한 칩에 여러 개의 코어를 넣어서 컴퓨팅 성능을 향상하고 있다. 바야흐로 멀티코어의 시대인 것이다!

진화압으로서의 멀티스레딩 프로그래밍

마이크로프로세서 자체의 속도가 빨라지던 시대에는 프로그래머는 18개월마다 소프트웨어가 2배씩 빨라지는 이점을 공짜로 얻었다. 하지만 멀티코어의 시대가 되면서 프로그램의 속도를 올리기 위해서는 여러 개의 코어에 작업을 직접 할당해야만 한다. 즉 성능 향상을 위해서는 멀티스레딩 프로그래밍 작업을 해야 하는 것이다2). Herb Sutter는 이를 두고 공짜점심은 끝났다 라고 하였다.

IT 산업에서 멀티코어 시대라는 환경의 변화는 멀티스레딩 프로그래밍이라는 강력한 진화압으로 작용하고 있다. 이제 살아남기 위한 여러 가지 새로운 기술들이 여기 저기서 나타날 것이고, 도태와 적자생존이라는 또 한편의 게임이 시작된 것이다.



함수형 프로그래밍

멀티스레드 프로그래밍의 어려움

멀티스레드 프로그래밍은 어렵다. 대부분의 프로그래머들이 스레드를 다룰 때 어려움을 호소한다. 아무리 뛰어난 프로그래머라 하더라도 100% 정확하게 동작하는 높은 병렬성을 가진 멀티스레딩 코드를 작성하는 것은 불가능하다. Unreal이라는 게임 엔진을 만든 천재 프로그래머 Tim Sweeney조차 멀티스레딩 프로그래밍의 어려움을 호소했다.3) 이 어려움을 어떻게 극복할 것인가가 당연히 많은 선도적인 프로그래머들의 고민이 되었다.

함수형 프로그래밍에 주목하다

No Silver Bullet 라는 기사에서 Fred Brooks는 소프트웨어 개발에서의 우연적 복잡성과 본질적 복잡성을 구분하면서, 우연적 복잡성의 해결은 고수준 언어의 역할이라고 했다. 당시에는 포트란이 어셈블리 언어에 내재되어 있는 우연적 복잡성을 제거하는 것으로 각광을 받았다. 그리고 자바와 같은 객체지향 프로그래밍 언어들이 C와 같은 절차형 언어를 그렇게 대체했다. 하지만 이제 시대는 멀티스레딩 프로그래밍의 어려움을 야기하는 우연적 복잡성에 직면해 있다.

그래서 요즘 주목받는 것이 함수형 프로그래밍이다. Common Lisp, Scheme, Haskell, Erlang 에 대한 관심도 그렇지만 F#, Clojure, Scala 등 현대적인 함수형 언어들이 등장하고 있다. 또한 객체지향 언어인 Java4)나 C++5) 등의 최신 버전에는 함수형 언어의 일부 기능을 속속 소개하고 있다. Javascript6)도 마찮가지다. 이렇게 주목받는 이유는 함수형 언어에서는 모든 데이타가 기본적으로 불변 데이타인데, 불변 데이타는 스레드가 아무리 많아도 그 접근에 아무 문제를 일으키지 않는다는 점이다. 함수형 언어에서는 멀티스레딩을 위해 Lock같은 접근제어 기능이 필요 없어 데드락이 발생하지 하지 않으며, 어느 스레드가 어떤 변수를 건드렸는지 다른 스레드가 고민할 필요가 전혀 없다는 점도 있다


클로져

함수형 프로그래밍 언어의 역사John McCarthy가 처음 Lisp을 만들던 1958년으로 거슬러 올라간다. 이후 Scheme (1975), Erlang (1986), Haskell (1990), Common Lisp (1994), OCaml (1996) 등으로 이어졌다. 1990년대와 2000년대 초반까지는 변방에서 명맥을 유지하다가 새롭게 부활하여 Scala (2003), F# (2005), Clojure (2007) 등의 VM 기반의 현대적인 프로그래밍 언어들이 탄생하게 되었다.

이처럼 많은 함수형 언어들 중 특히 클로져가 주목받는 이유는 무엇일까? 이에 대해 알기 위해 매우 많은 논쟁을 일으킨 Robert C. Martin의 The Last Programming Language7)라는 블로그 글을 읽어볼 필요가 있다. 그의 요지는 우리가 이제 거의 발명할 수 있는 모든 언어를 다 발명하였고, 새로운 언어는 뭔가 혁신적인 것이 아니라, 기존의 것을 약간 수정한 수준일 뿐이라는 것과, IT 분야가 현재의 기예의 단계에서 과학의 단계로 발전하기 위해서는 수학이나 화학에서처럼 단일한 언어를 가질 필요가 있다는 것이다. 이 최후의 프로그래밍 언어는 어떤 것일까? Robert C. Martin는 한 키노트에서 이에 대해 다음과 같이 말했다고 한다.

최후의 프로그래밍 언어는

  • 한 회사의 통제하에 있어서는 않된다.
  • 가비지 컬렉션을 지원해야 한다.
  • GOTO 문이 없어야 한다.
  • will constrain how we use assignment (more functional)
  • 다형성을 제공해야 한다.
  • 멀티패러다임이지 않아야 한다.
  • 가상 머신상에서 실행되어야 한다.
  • 기존 프레임워크를 사용할 수 있어야 한다.
  • 빨라야 한다.
  • 단순한 문법이어야 한다.
  • homoiconic 이어야 한다.

이런 기준에 의하면 일단 가상 머신에서 실행되어야 하기 때문에, 기존 함수형 언어들은 제외된다. 그는 이러한 조건을 만족하는 언어는 클로저라면서 Why Clojure?8)에서 다음과 같이 말했다.

  • 클로져는 현대적인 리스프이다: 리스프처럼 문법이 매우 간결. (F#이나 Scala는 C++처럼 문법이 복잡하고 기이하다)
  • 클로져는 JVM상에서 실행된다: clojure call java, java call clojure. CLR에서도 가능
  • 클로져는 STM을 기능을 제공한다: STM은 동시성에 대해 최신 솔루션이다.(thread와 lock을 대체한다)
  • 클로져는 빠르다: 메모리 공유 불변 데이타 구조 사용.
  • 다양한 지원: 튜토리얼과 블로그 IDE, 커뮤니티와 메일링 리스트 등

같은 글에서 그는 다음과 같이 말하면서 클로저에 대한 장밋빛 전망을 내놓았다.

“우리는 지금까지 수십년에 걸쳐 프로시져(Procedure절차형)에서 객체로 이전해 왔다. 이제 하드웨어의 물리적 제약이 비슷한 식으로 함수형 언어로의 패러다임 이동으로 우리를 내몰고 있다. 다음 몇 년 동안 우리는 어느 함수형 언어가 가장 좋은지 판별할 수 있는 다양한 프로젝트 실험들을 보게 될 것이다. 그 실험의 결과가 나올 때 나는 클로져가 매우 높은 위치를 차지할 것이라는 것을 완전 기대한다.”



클로저는 어떤 언어인가


클로저는 Rich Hickey에 의해 만들어 졌다. 그는 한 인터뷰에서 클로저를 만들게 된 동기에 대해 이렇게 말했다.

“우리는 기존의 객체지향 언어에 내재되어 있는 우연적 복잡성 때문에 매우 힘듭니다. 그 복잡성은 문법적인 것도 있고, 의미론적인 것도 있는데, 제 생각에 우리가 이런 사실을 전혀 모르고 있다는 거에요. 저는 '올바른 일을 한다는 것(doing the right thing)'9)이 단지 관례나 훈련에 의해 되는 것이 아니라, 저절로 자연스럽게 되게 하고 싶었어요. 저는 강고한 동시성과 기존 자바 라이브러리와의 거대한 상호운영성을 원했습니다.”

“복잡성은 저의 1차적 관심사인데요, 제 생각에 클로저는 이것을 잘 처리하고 있다고 생각합니다. 코어 수준에서 클로저는 매우 단순합니다. 클로저는 명백한 복잡성10)과 우연적 복잡성 둘 다 줄이는데 집중합니다. 특히 후자는 문제 영역이 아닌 해결 영역-언어나 도구-에서 나오는 복잡성입니다. 제 생각에 프로그래머들이 우연적 복잡성에 익숙해져 있는데요, 특히 단순한 것을 친숙한 것 혹은 간결한 것과 혼동하고 있지요. 그래서 복잡성을 만나면, 그것을 극복해야 할 도전과제로 여깁니다. 사실 그건 제거해야 할 장애물인데요. 복잡성을 극복하는 것은 잘 돼지 않습니다. 그건 낭비입니다.”

다음은 클로저가 지향하는 목표이다.

  1. 단순성 (Simplicity): 프로그래밍 언어는 프로그래머가 문제를 풀기 위해 사용하는 도구이다. 클로저는 도구가 만들어내는 우연적 복잡성이 없도록 하였다. 또한 본질적 복잡성을 좀 더 단순하게 만든다. 프로그래머는 그동안 친숙했던 복잡성에 길들여질 필요가 없다.
  2. 권능(Empowerment): 처음부터 다시 시작할 필요는 없다. 클로저는 오늘날 가장 많이 사용되는 플랫폼인 JVM상에서 구현되었다. 기존의 자바 코드를 그대로 사용할 수 있다.
  3. 핵심 (Focus) : 문제에 핵심에 집중하는 것은 생산성과 직결된다. 클로저는 도구의 문제가 아닌 원래의 문제의 추성성 수준에서의 문제를 표현하는 것을 가능하게 한다.


함수형 프로그래밍

객체지향 프로그래밍은 명사의 왕국(Kingdom of Nouns)이고, 함수형 프로그래밍은 동사의 왕국(Kingdom of Verb)이다. 명령형 프로그래밍(Imperative Programming)에서처럼 객체지향 프로그래밍에서 함수는 모든 일을 함에도 불구하고, 그 지위는 단지 Subroutine (필요에 의해 나열된 명령들이 묶인 것)이라는 2등 시민에 불과하다. 함수형 프로그래밍에서는 데이타와 같은 1등 시민 (런타임에도 생성되며, 함수의 인자로 전달되고 함수의 결과로서 리턴되며, 변수에 할당되는 것(entity))이라는 지위를 부여받은 함수는 조합가능성 (Composablility)이라는 능력을 획득하여, 명령행 프로그래밍에서는 발휘할 수 없었던 (혹은 어렵게 구현해야 하는) 놀라운 기능 (고계함수 (HOF, Higher-order function)을 이용한 Closure, Partial application, Currying 등)을 아주 쉽게 해낼 수 있게 된다.

순수 함수 (Pure function)

함수형 프로그래밍에서 함수는 수학에서의 함수 (예를 들어 sin함수)와 같은 의미인데 이를 순수 함수라고 한다. 순수 함수는 다음과 같은 특징을 갖는다.

  • 같은 입력에 대해 항상 같은 출력을 낸다 (참조투명성). 함수의 결과는 오직 입력된 파라미터에만 의존하며, 함수 외부의 어떤 값에도 의존하지 않는다. (전역변수에 의존하거나, 파일이나 네트웍으로부터 읽지 않는다.)
  • 함수의 실행이 부수 효과(Side effect)를 내지 않는다. 즉 프로그램의 어떠한 상태도 변경하지 않는다. (함수 외부의 변수를 변경하거나, 파일이나 네트웍으로 데이타를 의내보내지 않는다. 예외(Exception)도 부수 효과이다.)

프로그램의 상태는 프로그램의 코드에 의해 변경될 수 있는 데이타이다. 그런데 만약 코드의 결과가 상태에 의존한다면 모든 가능한 상태에 대해 코드의 행위를 추적한다는 것은 매우 어려운 작업이 되는데, 왜냐하면 프로그램의 다른 코드들의 부수 효과로 인해 그 상태는 변경되기 때문이다. 더욱이 동시성 문제가 개입되면 코드들의 실행 순서를 잡는 것은 쉽지 않아, 복잡성은 더 겉잡을 수 없는 수준이 된다.

순수 함수를 사용하면 이러한 사태는 없어진다. 순수 함수는 프로그램의 상태를 바꾸지 않으며, 그 자체로는 상태가 없다고 할 수 있다.

search?q=Pure%20and%20non-pure%20Functions.svg&btnI=lucky


순수 함수의 이점은 다음과 같다.

  • 순수 함수의 결과가 사용되지 않는다면, 그 순수 함수는 제거될 수 있다.
  • 부수 효과가 없기 때문에 같은 파라미터로 호출된 순수 함수는 두 번 호출될 필요 없이 그 결과만 저장하여 사용할 수 있다. (memoization)
  • 두 순수 함수 사이에 데이타 의존성이 없으면, 두 함수의 호출 순서는 문제가 되지 않으며, 병행적으로 호출되어도 서로 간섭하지 않는다. (thread-safe)
  • 순수 함수는 그 자체로 블랙박스이다. 사용자는 단지 입력과 출력이라는 인터페이스만을 생각하면 되지 내부 구현에는 전혀 신경쓸 필요가 없다. 그래서 순수 함수는 캡슐화와 재사용성을 완벽하게 지원하게 된다.
  • 이러한 속성으로 인해 순수 함수는 구성가능하게 된다. (Composability)

캡슐화와 재사용성은 OOP에서 객체를 통해 지원하려 했지만 완벽하게 보장할 수 없는 기능이다. 왜냐하면 객체는 자체의 상태를 갖고 있기 때문에, 메소드의 호출은 객체의 상태에 따라 달라지게 되는데, 이는 결국 외부에 어떤 식으로든 그 상태를 노출하게 된 것이 된다. 또한 상태를 갖는 객체는 구성가능하지 않다. 결국 객체를 구성하면서 만들어진 시스템은 매우 취약한 시스템이 된다. (HTTP는 요청과 응답으로 이루어진 Statelss protocol이었다는 사실에 주목하라)

클로저는 순수 함수형 언어가 아니다

프로그램이 순수 함수만으로 만들어 질 수는 없다. 화면에 표시하거나. 파일을 읽거나, 넷트웍으로 데이타 송수신을 하는 등의 부수 효과는 반드시 필요하다. 클로저는 함수가 반드시 순수 함수이기를 강제하지 않는다.(Haskell 같은 순수 함수형 언어에서는 모든 함수는 순수 함수이다. Haskell에서 I/O는 Monad를 통해 이루어진다11)). 클로저에서는 다음과 같은 타협점을 취한다.

  • 클로저에서 부수 효과는 분명 허용되지만 기본이라기보다는 예외에 속하는 것으로, 함수형 프로그래밍이라는 전체적인 흐름에서 눈에 띄게 표가 나기 때문에, 프로그래머들이 그것이 언제 어떻게 영향을 미치게 되는지 정확하고 분명하게 알게 된다.
  • 클로저는 모든 프로그램의 상태를 스레드로부터 안전하게 지키기 위해, STM(Software transactional memory)과 동시성 관련 레퍼런스 타입들을 사용하여 프로그램의 상태 변경이 안전하고 원자적으로 가능하게 한다.


불변 데이타

클로저에서 모든 데이타는 불변 데이타이다. 데이타는 한 번 생성되면 변경 불가능이다. 불변 데이타는 순수 함수와 불가분의 관계이다. 만약 순수 함수의 파라미터가 불변 데이타가 아니라면, 그래서 함수 실행중에 다른 곳에서 파라미터의 값이 변한다면, 그 함수는 순수 함수가 될 수 없다.

불변 데이타의 이점

불변 데이타의 이점은 다음과 같다.

  • 스레드 안전: 불변 데이타는 변경이 않되고 읽기만 가능하기 때문에 언제 어디서나 참조해도 전혀 문제가 되지 않는다. Java Concurreny in JAVA의 저자 Brian Goetz는 “불변 객체는 항상 스레드에 안전하다” 라고 하였다.
  • 부수 효과 제거: 불변 데이타는 부수효과를 원천적으로 제거한다. 그래서 순수 함수 프로그래밍이 가능해진다.
  • 쉬운 테스트: 테스트의 목적은 변화 대응이다. 변화가 적으면 테스트도 적어진다. 불변 데이타의 사용은 변화되는 데이타의 영역을 최소한의 영역으로 줄여 테스트가 쉬워진다.

불변 데이타는 다른 언어에서도 권장되고 있다. Effective Java의 저자 Joshua Bloch는 “클래스는 변화해야 할 분명한 이유가 없다면, 변경 불가능해야 한다. 만약 클래스가 불변일 수 없다면, 그 변경 가능성을 제한해야 한다.”라고 하였다. 실제 Java의 String, Integer, Long등 클래스는 불변 클래스이다. Scott Meyers의 Effective C++의 Item 3는 “Use const whenever possible”이다.

존속성 데이타 구조 (Persistent Data Structure)

하지만 프로그램에서 데이타를 변화시킬 필요가 있을 때는 어떻게 할까? 클로저는 이것을 존속성 데이타 구조 (Persistent12) Data Structure)로 아주 효과적으로 지원한다. 존속성 데이타 구조란 데이타의 갱신이 기존 데이타의 수정을 통해서가 아니라 기존 데이타를 그대로 유지한 채 신규 데이타와의 차이만을 추가함으로서 이루어지는 데이타 구조를 말한다. 데이타의 갱신은 데이타의 수정이 아니라 데이타의 차이의 추가인 것이다. 이 부분에 대해서는 데이타 구조에서 자세히 소개할 것이지만 간단한 리스트의 경우를 보자.

(def a (list 1 2 3))
(def b (conj a 0))

a 라는 리스트가 만들어 진 후, a에 0을 추가하여 b를 만드는 과정이다. list는 가변 인수를 받아 클로저의 리스트 자료구조를 만든다. 이것은 a라는 심볼로 지정된다. conj는 a 리스트에 0를 추가하여 새로운 리스트를 만들어 내는데, 이것은 심볼 b에 지정한다. 이 과정을 그려보면 다음과 같다.

search?q=perisitent-list.svg&btnI=lucky
리스트 b는 리스트 a와 자료 구조를 공유하고 있다. 리스트 b는 리스트 a의 값을 전부 복사하는 것이 아니다. 리스트 a의 기존 값들에 0를 추가하여 새로운 리스트를 만들어 낸 것이 리스트 b이다.

객체지향은 아니지만...

클로저는 객체지향 언어가 아니다. 그 자신이 오랜 동안 C++과 Java를 사용했던 프로그래머였지만, Rich Hickey는 특히 멀티스레딩 프로그래밍할 때, OOP 언어의 한계를 절감했다고 한다. 앞에서도 설명했듯이 멀티스레딩 프로그래밍의 어려움의 근본적인 원인은 데이타의 변경 가능성에 있는데, 프로그램의 상태를 바꾸는 코드들을 나열하는 명령형 프로그래밍의 한계를 OOP가 극복하지 못하기 때문이다.

객체 지향 프로그래밍의 문제점

  • God object 혹은 God Class의 문제가 있는데, 즉 한 객체가 너무 커서, 너무 많은 전역 변수를 사용하는 명령행 프로그래밍이 되어버린다.
  • 물론 이건 숙련도의 문제일 것이다. 잘 숙련된 프로그래머는 OOP 설계를 하고, OOP 5대 원칙(SRP, OCP, LSP, ISP, DIP)등을 지키며, 디자인 패턴을 적절히 사용하고, 밥 아저씨의 클린 코드에 입각하여 프로그래밍 할 것이다. 하지만 그래도 문제는 여전히 남는데, 변경 가능성은 서로 의존관계를 갖는 객체들을 통해 퍼져나간다. Stuart Holloway는 심지어 이렇게 말한다. “Design Patterns are a disease, and Clojure is the cure.”13) Paul Graham은 패턴을 sign of trouble이라고 했는데, 코드는 오로지 문제만을 표현해야지, 코드에 어떤 규정성을 둔다는 것은 뭔가 서투른 추상성을 사용하고 있다는 신호로 여긴다고 했다.14)
  • OOP의 또 하나의 문제는 모든 것을 객체로 본다는 것이다. 인간의 사고는 결코 객체만으로 이루어지지 않는다. 객체보다 더 하위인 '값(Value)'에 접근하면 문제는 더 단순해진다.(참고. Kingdom of Nouns)

데이터의 불변성을 보장하지 않는 것은 비단 OOP 언어들만의 문제는 아니고 지금까지 대부분의 언어들도 마찬가지다. 이것은 시간에 대한 잘못된 인식에 기인하는데, 동시성에서 자세히 살펴볼 것이다.

다형성은 좋은 것

OOP의 좋은 측면도 있다. 그런 점은 클로저도 지원한다.

  • 다형성 : 다형성은 함수나 메소드가 객체의 타입에 따라 서로 다른 정의를 갖는 것을 말한다. 클로저는 다형성을 멀티메소드와 프로토콜을 이용해 지원하고 있는데, 보다 개방적이고 확장성이 높다.
  • 캡슐화 : 캡슐화는 구체적인 구현을 인터페이스로 숨기는 것이다. 부수효과가 없는 클로저의 함수는 무엇을 하는지에 대한 관점이 아니라 무엇을 입력받고 리턴하는지에 대한 관점에서 볼 수 있으며, 함수 안에서는 블럭레벨 캡슐화와 로컬 캡슐화가가 가능하다.
  • 모듈화 : 자바의 클래스와 패키지가 하는 모듈화를 클로저는 이름 공간으로 지원한다.


괄호, 괄호, 괄호

클로져는 리스프의 적통이어서 괄호를 사용하여 코드를 작성한다. 다음은 전형적인 클로져 코드이다.

(println (+ 1 2 3 4 5))
;>> 15
;=> nil

리스프에 익숙하지 않은 프로그래머들에게는 이러한 표현이 낯설 것이다. 그러나 이 표현은 여러 가지 장점이 있다.

괄호 표현은 자바나 C++의 {}나 파스칼의 begin … end와 같이 스코프를 구분한다. 다만 함수명이나 특수구문(다른 언어서의 제어문)이 괄호 안에 있을 뿐이다. 클로져의 (println “hello”), (if true …)은 자바의 println(“hello”), if(true) {…}와 같다. 다만 함수 println이나 if가 괄호 안에 있느냐 아니면 밖에 있느냐의 차이일 뿐이다.

괄호 표현이 좋은 또 한가지는 연산자 전방표기다. 전방표기가 되지 않는 다른 언어에서는 (1 + 2 + 3 + 4 + 5) 처럼 + 연산자를 여러 번 사용해야 한다.

또 괄호 표현은 연산자 우선순위를 알 필요를 없앤다. 자바에서는 a & b « 3에서 어떤 연산자가 먼저 실행되는지 알아야 할 것이다. 이것은 잠재적인 버그 양산 가능성을 야기한다.

괄호는 리스트를 만들어 내는데 이것은 함수 호출 표현이다. 리스트에서 첫번째 요소는 함수로 취급되고 나머지는 인수가 된다.

괄호 표현이 처음에는 매우 이상하고 낯설겠지만, 일단 익숙해지면 그것이 매우 강력한 표현식이라는 것을 깨닫게 될 것이다. 왜냐하면 괄호 표현은 code as data(곧 설명됨, 즉 코드를 데이타처럼 취급할 수 있다!)라는 또 하나의 강력한 추상화를 가능하게 하기 때문이다.

S-expression

괄호를 사용하는 이러한 표현 방식은 리스프의 창안자 John McCarthy에 의해 만들어졌는데 이를 s-expression 혹은 sexprs (symbolic Expressions15))라고 한다. S-Expression은 코드와 데이타를 같이 표현하기 위한 간단한 방법인데 다음과 같다.

  1. Atomic Symbol 은 S-Expression 이다.
  2. 만약 A과 B가 S-Expression이면, (A.B)은 S-Expression 이다.

Atomic Symbol은 영문자와 숫자등의 문자만 이루어진 스트링이다. (A.B)는 특히 Cons Cell이라고 하는데, Orderd Pair를 묶어낸다. 두 쌍이 묶여있는 순서가 중요한데, 처음 자리에 있는 것을 car (여기서는 A), 다음 자리에 있는 것을 cdr (여기서는 B)라고 한다16). S-Expression으로 리스트는 다음과 같이 표현된다.

  • () ; empty list. NIL로 표기. ()은 empty이기 때문에 더 이상 나누어질 수 없는 Atom17)이 되는 유일한 리스트이다.
  • (A) = (A.NIL)
  • (A B) = (A.(B.NIL))
  • (A B C) = (A.(B.(C.NIL)))

모든 클로져 코드는 평가되는 문구(form)이다

일반적으로 유효한(평가가능한) s-expression을 문구(form)이라고 한다.18) 즉 (if conditon then else)는 if 문구, [1 2 3]은 벡터 문구이다. 하지만 (1 2 3)은 s-expression 이긴 하지만 문구는 아닌데, 리스트의 첫 요소인 1은 호출될 수 있는 것이 아니어서 평가될 수 없기 때문이다.

search?q=sep-form.svg&btnI=lucky

클로저에서는 코드가 곧 데이타이다. 이를 Code As Data라고 한다. 좀 어려운 말로는 Homoiconicity라고 하는데, 이것은 언어의 데이타를 표현하는 방식과 그 언어의 코드를 표현하는 방식이 같다는 것을 말한다. Lisp 계열 언어에서와 마찬가지로 클로저는 s-expression이라는 방식으로 코드와 데이타를 표현한다. 이것은 강력한 Metaprogramming 을 제공하는데, 클로저의 매크로는 이에 기반한 것이다. 19)

클로저에서는 코드를 s-expression으로 표현하며, 이를 문구(form)이라고 한다. 문구(form)는 어떤 값으로 평가되는 코드이다.

Clojure Forms

클로저 프로그램의 기본 단위는 문구(form)이다(라인, 키워드, 클래스가 아니다). 문구(form)은 평가되어 값을 리턴할 수 있는 코드 단위이다. REPL에서 뭔가 코드를 넣으면 그것이 평가되어 값을 리턴한다. 클로저 소스 파일은 문구(form)으로 구성된다. 문구(form)에는 4 종류가 있다.

  • 리터럴 : 리터럴은 그 자신으로 평가되는 문구이다. 스트링, 수, 문자등이 리터럴이다.
  • 심볼 : 심볼은 자신이 가리키는 값으로 평가되는 문구이다. 심볼은 마치 다른 언어에서의 변수처럼 보일 수 있는데, 변수는 아니다. 심볼은 함수의 인자나, 전역적으로 혹은 지역적으로 정의된 값을 식별하는데 사용된다.
  • 합성 문구 (Composite Forms) : 합성 문구는 소괄호(), 중괄호{}, 대괄호[]으로 다른 문구(Form)으로 묶어서 만들어진 문구(Form)이다. 어떤 괄호로 묶이느냐에 따라 합성 문구의 값이 달라지게 되는데, 특히 소괄호()로 리스트가 된다. 클로저에서 리스트는 함수 호출로 평가되는데, 리스트의 첫 요소는 함수로 나머지는 인자가 되어 함수 호출이 이루어진 후 그 결과값이 문구의 리턴값이 된다.
  • 특수 문구 (Special Forms) : 특수 문구 (Special Forms)는 합성 문구인데, 첫 요소는 클로저에서 내부적으로 정의한 정의된 함수이다.

다른 언어에서는 if, for, break 등은 프로그램의 동작을 제어할 뿐이지 어떤 값으로 평가되지 않는 반면, 클로져에서는 항상 어떤 값으로 평가되는 특수 문구(special form)이다.

(if true :a :b)
;=> :a

심지어 값도 항상 평가되는데, 자기 자신으로 평가된다.

43
;=> 43
[1 2 3]
;=> [1 2 3]

물론 연산자는 연산 결과로 평가된다.

(+ 1 2 3)
;=> 6

심볼의 정의는 심볼로 평가된다.

(def a 13)
;=> #'user/a
 
'a
;=> a
 
(defn average [& numbers]
  (/ (apply + numbers) (count numbers)))
;=> #'user/average
 
'average
;=> average

그리고 심볼은 그 값으로 평가된다.

a
;=> 13
 
average
;= #<user$average user$average@121f653>

물론 함수의 호출은 함수의 리턴값으로 평가된다.

(average 3 4 5 6 7)
;=> 5  


자바 플랫폼

자바 가상 머신

Clojure는 JVM상에서 실행된다. Clojure 코드를 컴파일하면 JVM상에 실행할 수 있는 .Class 파일이 된다. (클로저는 닷넷에서도 돌아간다(ClojureCLR). 브라우져에서도 돌아간다(ClojureScript).)

자바 인터롭 (Java Interop)

Clojure 프로그램에서는 기존의 자바 라이브러리를 바로 사용할 수가 있다. 기존 자바 코드에서 했던 것을 어떤 랩핑 작업없이 바로 클로저 코드해서 할 수 있다.

1)
10GHz까지 동작 주파수를 높일 목적으로 설계된 펜티엄4는 90nm 공정을 사용한 프레스캇 코어에서 결국 발열 문제로 4GHz의 벽을 넘지 못하고 좌초했다고 한다. 참고 멀티코어 세상에서 살아남기
2)
최근 인텔은 안드로이드가 멀티코어를 제대로 활용하고 있지 못하다고 의심하는 기사를 냈다Intel Questions Android Multi-Core Efficency; Who’s To Blame?
3)
“C++ 언어로 멀티스레딩을 지원하는 것은 어셈블리 언어로 객체지향 소프트웨어를 작성하는 것만큼이나 부자연스럽다.” 참고 The Quest for More Processing Power, Part Two: "Multi-core and multi-threaded gaming"
7)
2013년 1월15일 시점에 'The Last Programming Language Uncle Bob'으로 구글링했더니 83,700의 검색 결과가 나왔다.
8)
사실 그의 말대로 클로저가 최후의 프로그래밍 언어가 될 것이고 생각할 수는 없을 것이다. 아니 모든 프로그래머들이 하나의 언어만을 쓰자고 동의할 수도 없을 것이다. 마틴 파울러는 JavaScript가 최후의 언어일 것이라고 비꼬기도 한다. 다만 클로저가 최후의 언어로서 언급되고 있다는 사실에 의의를 두자.
9)
영어의 right은 도덕적으로 '올바른'이라는 뜻도 있지만, '정확한', '적절한'의 뜻도 있다. 기술적으로 '적절하고 정확한' 일을 한다는 것은 프로그래머에게는 도덕적인 문제기도 하다.
10)
이것은 Fred Brooks본질적 복잡성을 말한다. Fred Brooks우연적 복잡성만이 고급 언어를 통해 해결 될 수 있다고 하였다. 하지만 Rich Hickey는 언어를 통해 본질적 복잡성도 줄이는 것을 목표로 한다는 것이다. 이것이 어떻게 가능할까? 이를 위해 그는 우리에게 시간과 공간의 개념을 바꿀 것을 요구한다. 기존의 언어에는 차라리 시간과 공간의 개념이 없었다. 동시성이란 바로 시간과 공간의 문제이다. 즉 여럿이 같은 시간 같은 공간에 접근할 때의 문제인 것이다. 우리가 시간과 공간의 개념을 바꾼다면 동시성 문제는 단순해진다.(이것은 뉴튼의 절대적 시공간 개념이 아인쉬타인의 상대적 시공간 개념으로 바뀐 것과 같은 패러다임 전환이다. Rich Hickey의 시공간 개념은 객체지향과 이전의 프로그래밍 언어의 플라톤주의적 시공간 개념을 화이트 헤드의 시공간 개념으로 대체한다.)
12)
Persistent 라는 개념은 원래 메모리 내의 데이타가 메모리의 휘발성으로 사라지는 것을 막기 위해 파일이나 데이타베이스에 데이타를 보관하는 것으로 보통 '지속성' 내지 '영속성'으로 번역되었다. 하지만 여기서는 좀 다른 의미인데, 데이타의 변경이 이전 값을 유지하고 있는 것을 말한다. 즉 기존의 값이 지워지지 않고 유지 보존된다는 의미를 살리기 위해서 '존속성'이라는 번역어를 채택했다.
16)
LISP이 처음 구현된 IBM 704 컴퓨터의 하드웨어는 36비트의 워드를 Address(비트), Decrement(15비트), Prefix(3비트), Tag(3비트)로 사용했는데, car는 Content of the Address part of Register number, cdr은 Content of the Decrement part of Register number를 가져오는 어셈블리 매크로였다고 한다.(참조, CAR and CDR). car와 cdr의 유래에 대한 논의는 The origin of CAR and CDR in LISP 참고
17)
Atom은 그리스어로 A(not)-tom(divide) '나눌수 없는'이라는 뜻이다
18)
s-expression은 단순히 복잡한 것을 표기하는 방법이지만 이것이 수학의 알고리즘을 표현하는데 사용될 수 있다는 것은, 특히 mathmetical formalism과 깊은 관계를 갖는다. 참고 LISP: A Formalism for Expressing Mathematical Algorithms
19)
만일 XML이나 JSON 같은 자료 표현 방식으로 코드를 표현하는 언어가 있다면 그 언어도 Homoiconicity라고 할 수 있다. 이런 점에서 기계어는 Homoiconicity인데, 그 외에 Postscript, XSLT, XQuery, Prolog, R, Factor, Io 등이 있다.
lecture/clojure/why_clojure.txt · Last modified: 2019/02/04 14:26 (external edit)