'전체'에 해당되는 글 544건

  1. 2009.02.06 LISP에 관한 생각들 -세번째이야기
  2. 2009.02.01 시간의 경제학-10년 동안 프로그래밍 배우기
  3. 2009.02.01 LISP에 관한 생각들 -두번째 이야기
  4. 2009.02.01 LISP에 대한 생각들 -첫번째 이야기.
2009.02.06 02:55

LISP에 관한 생각들 -세번째이야기

 

해커 문화의 뿌리를 찾아서 Part 3: 해커리즘의 문화



안윤호안윤호 mindengine@freechal.com

필자는 아마추어 리눅스 커널 해커였으며 최근에 팹(Fab)이라는 책을 번역 출간했다. 컴퓨터의 여러 분야에 관심이 많고 컴퓨터와 문화의 인터페이스에도 관심이 많다.


2007년 6월 5일


연재순서
1회(2007년 4월): 리스프가 탄생하기까지
2회(2007년 5월): 원시 리스프의 재구성
3회(2007년 6월): 해커리즘의 문화


누가 무언가를 아주 좋아한다고 생각해 보자. 그리고 그 일이 컴퓨터나 프로그래밍에 관한 것이라고 범위를 더 좁혀보자. 예술 또는 다른 분야의 일이나 취미라도 상관은 없다. 그리고 그 일에 지속적으로 몰입하는 사람들이 몇 명 더 있다고 하자. 여기에서 공통적인 문화가 하나 탄생한다. 일종의 하위문화(subculture)라고 할 수도 있는 이 문화는 그것이 어떤 것이건 빠져있는 사람들에게는 아주 진지한 것이다. 보는 각도에 따라 진지한 놀이처럼, 때로는 진지한 일처럼 보일 수도 있다(일과 놀이의 심리적 요소가 아주 비슷하다는 사실은 예전부터 알려져 왔었다).

만약 이 문화가 주류 문화라면 우리가 매일 열심히 일하는 세상 일이 된다(일상적인 세상 일이 아주 재미있는 경우는 흔치 않지만). 주류 세계와는 조금 다른 일이 더 재미있을 수도 있다. 비주류나 반문화적인 요소가 있는 문화가 수용되는 일도 꽤 많이 존재했다.

현재 IT 문화의 일부 역시 기묘한 하위문화로부터 출발했다. 해커리즘도 그 중 하나다. 당연히 초창기에는 컴퓨터를 만지는 사람들이 이상하게 보이거나 이단적으로 보였다. 그것도 (대기업이나 연구소 직원으로서가 아니라) 정말로 컴퓨터가 좋아 컴퓨터에 빠져든 사람들은 사회의 일반적인 시각에서 보면 분명히 이단적인 요소가 있었다.

스티븐 레비의 책 『해커 그 광기와 비밀의 기록(원제는 Hackers: Heroes of the Computer Revolution)』은 이러한 이야기를 풀어나갔다. 초창기 MIT AI 연구소를 중심으로 한 1세대 해커와 컴퓨터를 사람들에게 보급시킨 2세대 해커들의 이야기가 책의 3분의 2를 채우고 있다. 1세대 해커가 리스프(LISP) 해커와 미니컴퓨터 해커라면 빌 게이츠나 스티브 잡스 같은 문화 아이콘들은 2세대 해커에 속한다.

『해커…』라는 책이 나온 지 이미 20년 정도가 지났고 컴퓨터가 하나의 주류 문화 정도가 아니라 문명의 근간이 되면서 이런 사람들의 이야기가 수용되기 시작했다. 세월이 더 지나면 이야기를 아름답게 적을 수 있는 동화작가의 손을 거쳐 미화되어야 할지 모른다. 사람들이 기대하는 문화코드는 보통 그런 것들이고 그런 책들이 서점에서 잘 팔린다. 나중에 아이들에게 이야기해줄 때도 껄끄럽지 않을 것이다. 그러나 새로운 문화나 하위문화가 항상 주류 문화에 대해 껄끄러웠다는 점은 인정해야 한다. 이들이 메인스트림으로 변한다 해도 초기의 그 이상한 에너지와 열정은 신비로운 것이며 다른 혁명이나 문화 활동에서도 공통적으로 나타났던 일들이다.

역사적으로도 비슷한 예가 있다. 산업혁명이 초기에 당시로서는 지극히 이단적이던 유니태리언파 사람들이나 프리메이슨, 아니면 그에 못지않게 비표준적이던 사람들에 의해 시작되었다는 것을 역사책에서는 잘 다루지 않는다. 교육 배경이나 사회적 지위가 아니라 스스로의 생각에 따라 움직일 수밖에 없던 사람들은 언제나 많은 것이다. 이들을 산업기계문명의 해커라고 불러도 하나도 이상한 일이라고 할 수 없을 것이다.

컴퓨터 문화의 전부는 아니더라도 중요한 요소들이 컴퓨터에 빠져있던 사람들의 정서를 반영한다. 세속적인 일이 아니라 컴퓨터에 빠져있던 이상한 사람들의 이야기는 방송으로도 나왔다. 예전에 미국 PBS에서 크린즐리의 ‘Triumph of the Nerds’라는 프로그램을 방송한 적이 있다. 이 프로는 상당한 인기를 모았다. 주식 시장에서 IT 주식이 마구 오르던 시절이어서 이 프로그램은 사람들의 반발을 사지도 않았다. 기묘한 괴짜인 너드들이 영웅시되기 시작했다. 결국 한때 괴짜 같던 컴퓨터 문화가 중요한 무엇으로 인정받았음을 의미한다. 컴퓨터 문화라는 것은 이상한 일도 아니었으며 예전과 달리 중요하지 않다고 생각하는 사람도 별로 없다.

산업혁명의 발전이 만들어낸 변화들이 모두 밝은 측면만을 가지는 것은 아니며 러다이트 운동이나 다른 노동운동 문화를 만들어낸 것도 사실이다. 컴퓨터에 대해서도 사람들이 반드시 밝은 측면만을 바라보는 것은 아니다. 그러나 초창기에 결정적인 변화를 만들어낸 사람들은 그들만의 강렬한 에너지가 있었다. 산업혁명이나 르네상스와 같은 시절에도 분명히 어떤 사람들의 강렬한 지적인 에너지가 있었다.

물론 해커들만 이상한 것은 아니다. 그들 주위의 일상도 정상적인 것은 아니었다. 1세대 해커들이 활동한 1960년대와 1970년대의 사회 분위기를 정상으로 보기도 어렵다. 그때가 좋은 시절이었는가를 생각해 보면 당시 사회의 광기어린 면들을 지적하지 않을 수 없다. 2차 대전이 끝나고 미국과 소련이 주도하는 냉전이 최고조에 달했던 시절이다. 군부 출신 대통령 아이젠하워가 걱정스러워 할 정도로 비대해진 군산복합체가 있었고 언제라도 핵전쟁이 터질지 모른다는 불안한 생각이 사회를 지배하던 시절이었다. 그리고 방위에도 과학기술 발전이 필수적이라는 것을 알게 되면서 이들의 돈이 컴퓨터를 연구하는 분야에도 투자되었다. 소련이 인공위성을 먼저 발사하면서 갑자기 엄청난 돈이 과학 연구에 투자되었다. 당시 사회에도 약간 미쳐 보이는 요소가 없는 것은 아니었다.

고가의 장비이던 컴퓨터를 만지는 사람들도 이 자금으로부터 무관하지는 않았다. 양심을 지키기 위해 최선을 다했다 해도 그렇다. 컴퓨터를 하려면 주류 문화의 설비를 이용하지 않을 수 없었다. 당시 지배층의 아이디어와 사회 통제에 대한 집착이 실제로는 누구의 생각과 이해관계를 반영하는가에 대해 생각하지 않을 수 없다. 그리고 악의에 찬 지배와 관리에 대한 집착이 아니더라도 그 집착에 이르게 한 판단의 근거들이 옳았는지를 생각하지 않을 수 없다. “옳다와 그르다”의 경계점은 항상 애매하기 때문에 근거에 대해 생각해보지 않을 수 없는 것이다. 사회 규정이나 규범에도 언제나 버그가 꽤 많이 존재한다.

요즘 세계화와 지적 자본주의 속에서 우리의 활동에 대해 후세에 어떤 비평을 들을지는 미지수다. 자유롭게 보이는 우리의 IT 산업과 문화는 복잡한 관계의 거미줄 속에서 자유롭지 않다. 회사와 기업에서 발생하는 새로운 통제는 얼마든지 우리를 얽매고 있을 수 있다. 실제로 기업들은 생존을 위해 여러 가지 일을 해왔던 것도 사실이다. ‘옳다와 그르다’ 또는 ‘정상적이거나 묘해 보이는’ 일들도 항상 새롭게 생각해 보지 않을 수 없다.

비판의식을 갖건 비판의식을 갖지 않건 하위문화는 강렬한 관심과 집중으로 만들어지는 작은 종교집단처럼 보인다고 한다. 사람들이 빠져있는 관심의 원은 그 자체가 하나의 작은 세계로 다른 세계와 구별된다. 완전히 빠져든 사람이 있다면 그것이 세상의 중심이다. 그리고 이런 중심은 도처에 있다. 영원하지는 않더라도 하위문화의 에너지는 쉽게 사라지지 않는다. 또한 이 에너지가 없다면 변화는 일어나지 않는다. 에너지 변화를 수반하는 몰입은 드문 일이 아니다. 사람들은 한때 문학에 열중한 적도 있었으며, 음악에 열광하거나 정치나 전쟁에 열광한 적도 있었다. 팝 문화나 TV나 영화에 빠져든 적도 있으며 모든 문화 영역은 이런 에너지로 넘친다. 글을 쓰다 보니 다른 문화와의 유사점만을 부각했지만 그것은 사실이다. 언제나 일어나는 현상인 것이다!

그 중에서 필자는 컴퓨터의 문화 원동력이었던 해커에 대해 이야기하고 있는 것이다. 우리가 과거의 해커들에 대해 어떤 식으로 규정하느냐는 바로 같은 업종에서 일하거나 관심을 갖는 우리의 일에 대한 새로운 해석을 필요로 한다(독자들은 이와 비슷한 말을 다른 역사책에서 이미 많이 들었을 것이다).



위로



하드웨어와 교감하다

초창기 컴퓨터는 많은 개량이 필요한 것이었다. 제작회사들이 아무리 멋있게 포장을 해도 당시 기계들은 요즘의 작은 IC 하나만도 못한 능력을 가졌다. 1초에 10만 번 덧셈을 할 수 있는 정도였다. 컴퓨터 하드웨어는 별것이 없었다.

하드웨어의 예를 들기 위해 오디오 앰프의 예를 들어보자. 오래전에 오디오라는 것은 별것이 없었다. 진공관 몇 개로 만든 앰프를 가지고 오디오광들은 신비스러운 음악의 세계로 빠져들었다. 당시의 명기라는 설계들도 회로로 보자면 오늘날의 기준에서 초라하기 그지없다. 주요 음원인 LP 판에서 나올 수 있는 소리의 질도 제한적인 것이었다. 그러나 많은 사람들은 빈약한 하드웨어의 경계를 넘어 소리에 빠져들었다. LP 판에서 나오는 잡음도 큰 방해가 되지는 못했다. 판을 너무 열심히 듣다 보면 골이 닳아버려 LP 판의 물리적인 수명이 다하곤 했다.

하나의 진공관으로 만들어진 앰프
하나의 진공관으로 만들어진 앰프. 사실상 트랜지스터 1개에 해당한다.

그러나 적어도 최소한도의 구현은 있어야 했다. 매우 초기에는 오디오를 듣는 사람과 만들고 개선하는 사람을 구분하기 힘들었다. 초기 오디오 설계와 구현은 많은 에너지의 집중을 필요로 했다. 하지만 최소한도의 세팅이 이루어지고 사람들이 이것을 좋아하기만 한다면 그 관심과 집중은 많은 변화를 만들어낼 수 있었다. 사람들은 수백 개의 음반을 듣고 수집하기도 하며 음반을 평가하고 관리하는 일도 큰 사업이라는 사실을 알게 된다. 레코드 시장도 커져갔다.

컴퓨터 역시 마찬가지였다. 일단 최소한도의 것들이 만들어지자 컴퓨터에 빠져드는 사람들이 늘기 시작했다. 당시 자료들을 검토하고 있자면 놀랄만한 일들이 한두 가지가 아니다. 1950년대는 말할 것도 없고 1960년대에 들어와 트랜지스터를 이용한 컴퓨터가 나오기 시작했을 때의 하드웨어도 빈약하기는 마찬가지였다. 예를 들면 플립플롭 회로 하나가 작은 책자 정도 크기였는데 레지스터의 1비트에 해당했다. 그러니까 책꽂이 하나가 1워드가 되는 셈이다. 커다란 컨트롤 패널이 보여주는 정보는 요즘의 디버거 한 줄의 정보에도 미치지 못할 때가 많았다. 프로세서 유닛의 명령은 다이오드와 배선으로 하드와이어 연결이 이루어져 있었다. 이 정도의 기계도 줄을 서야 사용할 수 있었다. 레지스터와 컨트롤 유닛, ALU 같은 것은 구조가 밖에서도 훤히 보였다.

초창기 PDP-1의 모듈의 일부
초창기 PDP-1의 모듈의 일부. 이 모듈은 NOT의 기능을 수행하고 이런 모듈들을 모아 PDP-1이 만들어졌다.

이 기계들을 사용하며 끊임없이 개량을 해보는 것이 해커들의 일이었다. 1~2명 정도의 영리한 엔지니어가 만들어낼 수 있는 간단한 컴퓨터와 당시의 빈약한 하드웨어 수준에서도 많은 일들이 이루어졌다.

TTL IC
몇 년이 지나자 TTL IC 한 개가 모듈의 기능을 대체할 수 있게 되었다.

교감하고 집중하는 것이 사실상 프로젝트의 성공과 실패를 좌우하는 일이다. 많은 프로젝트들은 사람들의 관심과 집중을 받지 못해 실패했다. 반대로 아주 빈약한 하드웨어라도 사람들의 힘과 정신력의 집중은 대단한 결과를 만들어낸다. 개발 프로젝트의 대상은 사람들과 교감한다. 개인적인 생각이지만 필자는 사람들의 머릿속 코딩이 기계의 코딩에 우선한다고 생각한다. 초창기에는 더 중요하다. 몇 년을 우회할 발전이 며칠 만에 해결되는 수도 있다. 그래서 우선은 사람들을 코딩되어야 한다.

어떤 일을 너무 좋아하는 사람들이 나타나면 일은 빠르게 진행된다. 컴퓨터가 이들에게는 세상의 중심이었다. 오늘날의 컴퓨터에 비하면 빈약한 하드웨어와 소프트웨어였으나 이것으로도 교감할 수 있었다. 하드웨어는 언제나 더 많은 개선을 필요로 했고 최고의 효율을 발휘해도 언제나 연산능력과 메모리는 부족했다.

초창기 컴퓨터라는 것은 비싼 장비였기 때문에 사용에 제약이 가해지기는 했으나 컴퓨터 제작회사의 엔지니어나 그것을 사용하는 해커들은 실험적으로 많은 해킹을 했다. 기술적으로 특별히 감출만한 것들도 없었다. 나중에 그 기계 사용자들이 회사에 입사해 새로운 컴퓨터들을 만들어 내기도 했다. 필자는 과연 당시 사용자들이 새로운 시도를 중지하고 제작회사에서 하라고 정한 일들만 하는 것이 옳았는지 아니면 왕성한 실험정신을 발휘한 것이 맞았는지에 대해 생각해 보아야 한다고 믿는다(물론 필자의 생각은 이단적일 수 있으며 생각하기에 따라 맞을 수도 틀릴 수도 있다).

이 정도의 하드웨어에서 지난번에 설명한 것과 같은 리스프 인터프리터가 구현되었다. 초기에는 리스프에 제한이 너무 많았다. 리스프가 상당히 개선된 것은 코톡이라는 해커가 설계한 컴퓨터에 그린블러트의 MacLISP가 구현되고부터다. 코톡은 나중에 DEC로 가서 PDP-6의 주 설계자가 되고 PDP-6은 나중에 PDP-10이 된다.

1970년대에 이미 36비트 컴퓨터가 존재했다. 다시 몇 년의 세월이 지나자 초기 해커들은 몇 명을 제외하고는 대부분 자리를 옮겼다. 컴퓨터가 세상의 중심이었던 사람들 역시 체력과 집중력이 약화되는 것을 몸으로 느꼈을 뿐만 아니라 사회적, 경제적인 제약으로부터 자유롭지 못했다. 초기 리스프 해커들의 전성기는 10년 정도 지속되었다.

PDP-1
PDP-1(www.computerhistory.org의 사진에서)



위로



람다 계산법(Lambda Calculus)

열정과 집중이 중요하다는 이야기를 너무 길게 한 것 같다. 이제 다시 리스프 이야기로 돌아오자.

리스프가 람다 표기법을 채택한 것은 지난번에 설명했다. 리스프 프로그램에 대해 자세히 설명하지도 않고 리스프 인터프리터를 만드는 이야기를 진행하였으니 황당하기는 하지만 실제로 리스프는 그렇게 갑자기 세상에 나타난 것이다.

리스프는 함수를 람다 표기법으로 나타낸다. 람다 표기법은 특별히 수나 기호를 구분하지 않는다. 람다 표기법은 조금 생소한 것이라 설명이 필요하다. 람다 계산법은 치환을 다루는 계산법이다. 전반적인 내용이나 배경이 위키백과에 상당히 잘 정리되어 있다. 필자는 람다 계산법을 설명하기 위해 ‘An Introduction to Lambda Calculus and Scheme’에 나오는 예제를 그대로 몇 개 인용해 보았다.

함수는 입력을 받는 부분과 결과를 내는 부분이 있다. 이제 우리가 어떤 대상에 초콜릿을 씌우는 함수를 갖고 있다고 생각하고 다음과 같은 것을 생각해 보자.

  
        peanuts ->     chocolate-covered peanuts 
        raisins  ->     chocolate-covered raisins 
        ants    ->     chocolate-covered ants 


이것을 람다 계산법을 사용하여 표현하면 다음과 같다.

  
        Lx.chocolate-covered x 


여기서 L은 람다(λ)를 나타낸다. 함수에 인자를 대입하는 것을 다음과 같이 표시한다.

  
        (Lx.chocolate-covered x)peanuts -> chocolate-covered peanuts 


람다 계산법에 따르면 람다식에 어떤 인자를 적용한 결과가 또 하나의 함수일 수도 있다. 초콜릿이 아니라 캐러멜을 포장할 수도 있는 것이다. 이를테면 아래와 같은 람다식을 만들 수 있다. 이 식은 y로 싸인 x를 만드는 것이다.

  
        Ly.Lx.y-covered x 


이제 캐러멜을 덮는 함수를 만들어낼 수 있다. 식은 y 인자로 캐러멜을 받았다.

  
        (Ly.Lx.y-covered x)caramel -> Lx.caramel-covered x 


그리고 이 함수는 다시 땅콩을 덮도록 만들 수 있다. x 인자로 peanuts를 받았다.

  
        (Lx.caramel-covered x)peanuts -> caramel-covered peanuts 


함수의 인자가 반드시 숫자일 필요는 없다. 람다 계산법에서 함수는 다른 함수의 인자가 될 수도 있다. 지난번의 간단한 인터프리터에서도 함수를 다른 함수의 입력으로 사용할 수 있었다. 아래 식에서 f 인자는 함수다.

  
        Lf.(f)ants 


그래서 초콜릿을 포장하는 함수를 ant에 적용할 수 있다. (apply-to-ants)를 잘 살펴보면 단순한 치환이 이루어지는 것을 알 수 있다. ()의 주변을 잘 살펴보라. f는 Lx.chocolate-covered x로 대체되었다. 그리고 x에는 ants가 적용되었다.

  
        (Lf.(f)ants)Lx.chocolate-covered x 
        -> (Lx.chocolate-covered x)ants 
        -> chocolate-covered ants 


함수가 한 람다식을 적용한 결과로 만들어질 수 있다는 것은 대단한 일로 많은 가능성을 갖고 있다. 이런 것을 클로저(closure)라고 부르기도 하며 리스프나 스킴(scheme)에서 고차함수를 만드는 바탕이 되었다.

자세한 증명은 람다식을 다루는 문헌들을 찾아보기로 하고 람다 계산법이 왜 일반적인 컴퓨팅의 원리로 변할 수 있었는지를 생각해 보자. 사실 컴퓨터는 구현 이전부터 만들어질 수 있는 방법이 있었다. 그 중 하나는 람다 계산법을 통해서다. 전기 스위치를 사용하건 다른 기계적인 무엇을 사용하건 만들어질 수 있었다. 구현 논리는 이미 수학적으로 존재했다. 우선 조건식을 람다함수로 만들어낼 수 있다. 이를테면 참과 거짓을 다음과 같이 만들어낼 수 있다.

  
        true = Lx.Ly.x 
        false = Lx.Ly.y 
        if-then-else = La.Lb.Lc.((a)b)c 


매우 생소하기는 하지만 논리식을 람다 함수로 표시해본 것이다. 몇 개의 치환을 거쳐 람다 함수는 조건식을 정확히 계산한다. 아래의 식은 if-then-else가 참이면 apple을, 거짓이면 banana를 돌려주도록 되어 있다. 미리 false를 적용한 것이라 banana를 돌려준다.

  
        (((if-then-else)false)apple)banana 
        -> (((La.Lb.Lc.((a)b)c)Lx.Ly.y)apple)banana 
        -> ((Lb.Lc.((Lx.Ly.y)b)c)apple)banana 
        -> (Lc.((Lx.Ly.y)apple)c)banana 
        -> ((Lx.Ly.y)apple)banana 
        -> (Ly.y)banana 
        -> banana 


간단한 정리 몇 개를 적용한 것치고는 많은 일을 할 수 있는 것 같다는 생각이 들지 않는가? 아무튼 앞서의 초콜릿과 캐러멜을 치환하는 식과 peanuts, raisins, ants를 치환하는 방법을 그대로 적용한 것이다. 종이와 연필로 계산해볼 수 있다. 지난번의 cons와 car, cdr 도 람다식으로 표현할 수 있다.

  
        cons = La.Lb.Lc.((c)a)b 
        car = Lx.(x)true 
        cdr = Lx.(x)false 


이런 방법으로 생각하는 cons, car, cdr의 정의가 나름대로 중요한 내용이라 실제로 SICP의 비디오 강의 5b에서는 중요한 개념으로 떠오른다. 강의에 나오는 스킴 식에서는 다음과 같이 정의했다.

  
(define (cons x y) (lambda(m) (m x y))) 
(define (car x) (x (lambda (a d ) a)) 
(define (cdr x) (x (lambda (a d ) d)) 


이 식을 실제로 수행하면 다음과 같다.

  
(car (cons 35 47)) 
->(car (lambda (m)(m 35 47))) 
->((lambda(m)(m 35 47)) (lambda(a d) a)) 
->((lambda (a d) a) 35 47) 
->35 


별것 아닌 것처럼 보이는 내용이겠지만 위 정의를 조금 더 변형하면 사이드 이펙트(side-effect)를 만들어낼 수도 있고 함수형 언어에서 덮어쓰기(assignment)의 메커니즘을 만들어낼 수도 있다. 재귀(recursion) 역시 람다식을 이용해 만들어낼 수 있다. Y 컴비네이터라고 부르는 것인데 리스프에서는 label을 이용한 다른 방법으로 구현했다.

리스프라는 언어는 이런 람다 계산법을 적용하는 하나의 이론적인 기계 그 자체이며(지난번에 설명한 간단한 evaluator 그 자체가 A4 용지 한 페이지 정도의 식이다) 리스트로 되어있는 다른 리스프 식을 읽어 이들을 치환하여 계산을 하고 경우에 따라 수식이나 새로운 함수 자체를 답으로 되돌린다.



위로



Paradigms of Artificial Intelligence Programming

초창기 해커들이 관여하던 몇 가지 문제들을 정리한 책이 있다. 리스프가 개발되고 문제의 표현을 위해 사용되던 곳이 인공지능(AI) 분야였기 때문에 당연히 리스프에는 좋은 예제가 많았다.

   소셜 북마크

   mar.gar.in mar.gar.in
    digg Digg
    del.icio.us del.icio.us
    Slashdot Slashdot

Paradigms of Artificial Intelligence Programming피터 노빅(Perer Norvig, http://Norvig.com)이 쓴 책 중에 『Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp』라는 유명한 책이 있다. 첫 회에 소개한 SICP보다 더 어려운 수준의 책(하드코어에 속한다고 하는 사람도 있다)이지만 리스프로 어떻게 인공지능 문제들을 접근했는지에 대한 좋은 자료다. 책의 2부는 초기 AI 프로그램을 다루고 분석하고 있다. 이를테면 GPS(General Problem Solver), ELIZA, 수학적 기호처리 같은 것들을 다룬다.

책을 사볼 필요는 없겠지만 http://norvig.com/paip.html에서 ‘Excerpts from the preface, including why Lisp?’ 같은 글들을 읽어 보기 바란다. 어려운 내용도 아니며 좋은 생각거리를 제공할 것이다. 그 외에도 좋은 글들이 꽤 많다.




위로


Trackback 0 Comment 0
2009.02.01 12:25

시간의 경제학-10년 동안 프로그래밍 배우기

예전에 zdnet 에 실렸던 글입니다.
 
안윤호(아마추어 커널해커) mindengine@freechal.com
2006.12.31 / AM 01:34

 
필자는 요즘 프로그래밍을 다시 배우고 있다. 어떤 광고 문구에 나오는 것처럼 그동안 2% 부족하다고 느끼던 부분들을 손대기 위해서다. 완벽한 것은 없지만 중요하다고 느끼던 부분들이다. 프로그래머 생활을 한 사람으로 컴퓨터 언어로 표현하는 능력에 무엇인가가 부족하다는 것은 좋은 일이 아니다. 표현능력의 부족은 코딩 스타일에 문제가 있다기보다는 프로그래밍을 배우는 과정에 문제가 있거나 문제에 대해 접근하는 방법에 문제가 있을 지도 모르는 것이다. 좀 더 심하게 말하면 생각하는 방법에 문제가 있을 수도 있다. 개인적으로는 중요한 문제다. 그렇다면 신중하게 접근해야 한다.

일의 발단은 10년 전에 LOGO로 프로그래밍하면서 느꼈던 기묘한 궁금증들을 나중에 생각해보니 상당히 중요한 문제였다는 것을 알게 되면서부터였다. 메시지를 보내는 여러 객체들의 역할을 다루는 actor model과 message passing의 문제였으며 일부는 lambda 함수의 문제이기도 했다. 그때나 지금이나 까다로운 문제다. 다시 붙잡고 보아도 과거보다 이해력이 좋아졌다는 증거는 없었다. 누구나 오래 생각해보던 문제들은 있는 법이다. 필자의 경우는 예전에 만들어보고 싶어 했던 어떤 에이전트를 표현할 수 없던 표현력의 문제를 갖고 있었다. 수학도 그렇고 코딩을 하는 것도 그랬다. 그런데 10년 동안 전혀 변화가 없었다.

많은 시간을 들인 일이 답 없이 끝날 수 있고 필요한 시간은 1년이 아니라 몇 년이 될 수도 있으며 그동안 많은 일들을 못할 뿐만 아니라 별로 생산성을 내지 못할 수도 있다. 그리고 무엇보다 머리는 자꾸만 더 나빠질 것이 확실하다. 비관적인 결론을 내리면 어떤 문제의 일반화는 1년이 아니라 10년이 걸릴 수도 있다. 다른 중요한 선택들처럼 판돈이 큰 도박이다. 그리고 기다라는 보상은 대박과 같은 것이 아니라 자기만족이다. 어느 정도 좋아서 하지 않으면 안되는 것이다.

아마추어 프로그래머가 이 정도의 스트레스를 느낄 정도라면 현장에 있는 프로그래머나 관리자의 시간적인 압박은 상당할 것임에 틀림없다. 하지만 현장은 해결할 수 있는 해결책 안에서 고민하는 것이라 압박의 성질이 다르다. 현장은 것은 할 수 있는 일을 열심히 하고 할 수 없으면 포기하거나 운에 따르는 수밖에 없는 곳이다. 현장에서는 쓸데없는 일로 고민하지 않는다. 고민할 시간을 주지 않기 때문이며 이번에 못한 일이라면 다음에 잘 하는 수밖에 없다. 프로젝트만 놓고 보자면 이번에 잘한 팀이 훨씬 유리해지는 게임이다. 물론 생각이나 공부는 그 프로젝트의 시간 안에서 일어난다. (예전의 학생에서 이제는 교수가 된 친구들이 자주 하는 이야기가 있다. 시험을 통과하지 않은 공부는 흔들린다는 것이다. 수업이 중요한 것이 아니라 시험 기간 동안 쌓이는 집중이 기본기를 만든다는 것이다. 다 수긍할 수는 없지만 많은 부분이 사실인 것은 어쩔 수 없다. 현실에서는 더 무서운 시험인 현장의 프로젝트가 있다.)

그러나 아무리 성공적으로 프로젝트를 진행한다고 해도 사람들마다 그 안에서 무언가 중요하다고 생각하던 주제는 반드시 있었을 것이라고 생각한다. 없었다면 그것은 너무나 바빴던 것이 틀림없다. 몇 번의 프로젝트를 진행하고 나면 보통 몇 년의 세월이 흐른다. 하던 일들은 대체로 비슷하기 때문에 문제를 일으키는 어떤 걸림돌 같은 부분이 있다. 개선이나 변혁을 기다리는 요소들이다. 성공적인 프로젝트를 오랜 기간 진행하고 나서도 경험곡선을 상승시킬 그 무엇이 없었다면 역시 무엇인가가 부족한 것임에 분명하다. 그리고 실패한 프로젝트는 원래 배울 것이 많은 것이니 말할 필요도 없다. 성공을 하건 실패를 하건 시간은 흘러간다.

피터 노빅은 왜 10년이라 했을까?
오랜 기간이 필요하다는 생각이 들자 첫 번째로 떠오르는 생각이 피터 노빅의 글이었다. 노빅은 작년까지 구글의 Search Quality의 책임자였다가 요즘은 연구 책임자로 있다. 구글에 오기 전까지는 NASA AMES의 컴퓨터분과 책임자로 있었다. 인공지능과 머신러닝 분야에서 잘 나가는 연구자이기도 했다. 피터 노빅의 글중에 10년동안 프로그래밍 배우기(Teach Yourself Programming in Ten Years)라는 유명한 에세이가 있다. 우선 제목을 잘 붙였다. “10년 동안 프로그래밍 배우기” 아니면 책방의 책들처럼 “프로그래밍 10년 완성”. 인상적인 제목이다.

피터 노빅
10년이면 상당히 긴 세월이다. 단 몇 줄을 읽고 싶지 않아 건너뛰기도 하는 필자와 같은 사람들에게 10년이면 거의 영원에 가까운 세월이다. 그러나 10년은 돌이켜 보면 금방 흐르는 세월이다. 프로그래밍이나 전자공학의 흐름에서 10년은 아주 긴 것 같으면서(많이 변한다.) 덧없이 금방 흘러버리고 마는 시간이다. 만약 이 10년이라는 시간이 잘 사용할 수 있으면, 그리고 성취의 흐름을 10년 정도로 잡는다면 그렇게 조급해 하지는 않아도 될지도 모른다. 만약 시간의 흐름을 1년이나 3년 정도로 잡는다면 약간 조급해 질지도 모른다. 1달이나 몇 개월의 흐름으로 잡는다면 조급해져서 복잡한 일은 할 수 없을 것이다. 세월이 아무리 빨리 흘러도 일들의 진행에는 시간이 필요한 법이다. 프로그래밍을 소화하는 정도가 아니라 밥을 소화하는 데에도 몇 시간이 필요하다. 필자 역시 조급해져서 하던 일을 중간에 팽개친 것이 한두 번이 아니기 때문에 10년이라는 척도를 다시 생각해 보았다.

정말 긴 시간이기는 하지만 어떤 일의 마스터링에는 대략 10년 정도가 걸린다는 것이 노빅의 주장이다.

글의 시작은 서점에 들어서면 “자바 7일 완성 (Teach Yourself JAVA in 7 days)” 과 비슷한 제목의 인터넷, 윈도우, 비주얼 베이직에 대한 책들이 끝없이 늘어선 것에 대한 비판으로 시작한다. 아마존을 검색해보면 이런 서적이 대부분 컴퓨터분야에 몰려있다는 것이다. 노빅은 이 검색의 결론을 컴퓨터를 배우려는 사람들의 엄청난 붐이 있거나 컴퓨터를 배우는 일이 너무나 쉽기 때문에 그럴지도 모른다고 했다. 물리학이나 베토벤에 대해서는 당연히 며칠 만에 배우기가 없으며 개의 손질법마저도 몇 일만에 배우는 책은 없다고 했다. 이를테면 “3일 동안 Pascal 배우기” 같은 책이 알려줄 수 있는 것은 Pascal과 비슷한 그 무엇이다. 비슷한 언어를 잘 알고 있다고 해도 그 동안에 배울 수 있는 것은 문법정도라는 것이다. 3일이나 일주일 동안에 일어날 수 있는 변화는 그것뿐이다. 인생이 바뀌지도 않는다고 한다.

노빅이 제시한 것은 “10년”이라는 긴 시간이었다. 10년이라는 시간을 잡은 데에는 이유가 있다. 보통 한 분야에서 정통하거나 대가가 되기까지 일반적으로 10년 정도의 세월이 걸린다는 연구 결과를 인용한 것이다. 체스, 음악 작곡, 미술, 피아노, 수영, 테니스, 정신심리학, 위상 수학 기타 다양한 분야에서 전문가가 되기 위해서는 보통 십년 정도가 걸린다는 것이 정설이라는 것이다. 지름길이나 단축코스가 없었다고 한다.

신동이라 불린 모짜르트도 최고의 음악을 만들기까지 13년 이상이 걸렸다. 비틀즈 역시 비교적 빨리 유명세를 타긴 했지만 훨씬 이전인 1957년부터 작은 클럽에서 활동을 시작했다. 결정적인 작품들은 10년 정도 지난 1967년 Sgt. Peppers에 이르러서야 만들 수 있었다. 그러니 일반적인 사람들 역시 목표의 눈높이는 낮을지 몰라도 어느 정도 정통해지기 까지는 당연히 세월이 필요한 것은 분명하다. 그리고 이 기간 동안 부단히 개선하고 노력해야 한다는 당연한 글을 쓴 것이다.

노빅의 프로그래밍을 배우는 방법
노빅이 제시한 프로그래밍을 배우는 방법은 아주 간단하다.

- 프로그래밍에 관심을 가져보고 정말 재미가 있다고 생각해야 10년 정도를 기꺼이 쏟아 부을 수 있다(필자의 생각에 이것은 당연한 것 같다.).

- 다른 사람의 프로그램을 읽어보고 다른 사람과 이야기해야 하는데 이 과정은 어떤 책이나 교육과정보다 중요하다(다른 사람의 작품을 읽어보지 않으면 당연히 다른 사람에게 읽힐 작품을 쓸 수 없다.).

- 가장 좋은 배우기는 실제로 해보면서 배우는 것이고 이 방법을 더 적극적으로 체계화해야 한다는 것이다. 최고의 성취는 오랜 기간 경험을 쌓으면서 생기는 것이 아니라 노련한 사람의 경우에 있어서도 끊임없는 개선으로 이루어지기 때문이다(이 과정에서 엄청난 에너지가 필요하다는 것을 알고 있다. 버전업과 점진적인 발전 앞에는 장사가 없다.).

- 컴퓨터 학과가 가르쳐주는 것이 전부가 아니며 일을 하면서 배울 수도 있다.

- 프로젝트를 다양하게 해보면서 어떤 프로젝트에서는 최고의 프로그래머가 되어 다른 사람들을 리드하고 비전을 제시해 보기도 하고 어떤 프로젝트에서는 다른 사람으로부터 지도 받을 필요가 있다(다른 사람에게 무엇인가를 가르쳐보는 것이 최고의 학습이라는 말이 있다.).

- 다른 사람이 이끄는 프로젝트에 참여하여 다른 사람의 프로그램을 이해한 후 원래의 작성자가 놓친 부분을 고쳐보기도 하고 자기의 프로그램을 관리할 다른 사람들이 쉽게 작업할 수 있는 프로그램을 작성하기도 해야 한다(작가의 표현력은 여러 번 고쳐 써 보면서 증가한다고 한다.최고의 글쓰기는 다시 써보기라는 말도 있다.).

- 다양한 프로그래밍 언어를 배워라(여러가지의 패러다임을 배울 필요가 있다.).


그리고 몇 가지가 더 있다.

읽다보면 10년 정도의 기간으로 부족할 것 같기도 하다. 10년 동안 이렇게 배우는 것은 10년을 열심히 살라는 이야기와 다를 것이 없어 보인다. 써보고 가르치고 배우고 참여하여 몸으로 체득하는 일을 게을리 하면 안 되는 것이다.,

프로그래밍은 스스로 인지하는 과정에서 배운다
노빅의 주장 가운데에는 몇 가지의 핵심적인 요소가 있다. 책이나 문서라는 것은 스스로 배우거나 사람들에게 배우는 것보다 훨씬 약하다는 사실이다. 때로는 책이나 문서를 읽는 것 보다 그냥 프로그램을 만들고 그 안에 빠져 들어가는 편이 더 낫다는 것이다. 그리고 많은 일들은 일 그 자체를 해나가는 과정이 바로 배우는 과정이라는 사실도 바로 이해할 수 있다. 어떻게 보면 책이나 문서라는 것은 이 과정을 배우는 데 필요한 하나의 주석이라고 볼 수 있다. 결국은 프로그래밍을 배우는 것은 하나의 커다란 인지과정이다. 다른 분야와 다를 것도 없다. 10년이라는 것은 소모되는 세월이기도 하지만 변화에 투입되는 기본적인 자원이다.

글의 뒷부분에는 노빅 자신의 이야기가 나온다. 노빅은 첫째 아이가 태어날 무렵 수없이 많은 "How to.." 책을 읽었다고 한다. 그러나 정말 어쩔 수 없는 초보자라는 느낌을 지울 수 없었다고 한다. 30 개월이 지나 둘째가 태어날 무렵 노빅은 책을 새로 읽은 것이 아니라 자신의 경험을 믿기로 했다. 그러자 자신의 경험을 바탕으로 하는 일이 전문가가 쓴 수 천 페이지의 책보다 유용하고 더 확실한 것이었다고 한다. 차라리 프로그래머에게 How To가 아니라 How Not To를 가르치는 편이 어떨까하고 이야기한다. 스스로 배운다는 것은 HowTo에 지배되지 않는다는 사실이다. 그래서 어떤 사람들은 잘 할 수 있는 일과 함께 하는 경우에 커다란 능력을 발휘할 수 있다고 한다. 소질을 타고나는 경우도 있다. .

노빅은 글의 끝머리에서 책방에 가서 책을 사서 읽으면 어떤 유용한 가르침을 줄지는 모르지만 그 것이 며칠이나 몇 달 안에 인생을 변화시키지도 않을 것이고 통달을 가르쳐 주지도 않을 것이라고 말한다. 당연한 말이다.

필자의 생각은 책의 제목이 Teach Yourself XXX in Ten Years 라고 쓰여 있다고 해도 답은 종잇장에 있는 것이 아니라 문제를 풀어나가는 사람에게 속해있다는 것이다. 실제로 생각하고 풀어보고 시간을 들여서 답을 만드는 사람 자체가 답이며 문제들과 함께 변한다. 꼭 정신수양의 과정처럼 보이지만 이런 것들을 잘 엮어서 생각해보면 답이 나올 것 같기도 하다. 그리고 한편으로는 필자 스스로의 변화에 대한 기대이기도 하다. 기대가 없으면 행동은 나오지 않는다.

필자의 생각에 이 게임에서 10년 동안 가르치고 배우는 사람은 바로 자신(Yourself)이다.@


Trackback 0 Comment 0
2009.02.01 08:13

LISP에 관한 생각들 -두번째 이야기


해커 문화의 뿌리를 찾아서 Part 2: 원시 리스프의 재구성



안윤호안윤호 mindengine@freechal.com

필자는 아마추어 리눅스 커널 해커였으며 최근에 팹(Fab)이라는 책을 번역 출간했다. 컴퓨터의 여러 분야에 관심이 많고 컴퓨터와 문화의 인터페이스에도 관심이 많다.


2007년 5월 8일


연재순서
1회(2007년 4월): 리스프가 탄생하기까지
2회(2007년 5월): 원시 리스프의 재구성


리스프(LISP) 개발 초기 역사에 대해 중요한 문서는 매카시 자신의 History of LISP이며 다른 하나는 H.Stoyan이 정리해 놓은 리스프의 역사로 둘 다 매카시의 홈페이지(http://www-formal.stanford.edu/jmc/history)에서 찾아볼 수 있다.

History of LISP에서 매카시는 너무 가벼운 마음으로 일을 진행한 것을 한탄하고 있다. 튜링머신보다 리스프가 더 좋으려면 만능 리스프 함수가 있어야 하고 이 함수는 튜링머신으로 적는 것보다 더 깔끔하고 이해하기 쉬워야 했다. 이것이 바로 리스프 함수 eval[e, a]이었다. 여기서 e는 리스프 식이며, a는 변수에 적어 넣을 값을 가지고 있는 리스트다. 매카시는 eval을 만들면서 리스프 함수를 데이터처럼 나타내는 방법을 만들어야 했고 이 표기법은 표기를 위한 목적으로 생각했을 뿐 실제로 리스프 프로그램을 위해 사용되리라고는 생각하지 못했다. 매카시가 함수 eval을 구현하기 위해 그리고 새로운 언어를 위해 할 일은 많았다. 일의 중요한 진척의 하나로 함수가 함수의 인자로서 사용될 수 있는 표기법을 논리적으로 완결하는 일도 필요했다. 매카시가 만든 일들 가운데에는 원래의 람다(lamda) 표기법에 없었던 것들도 있었다. 이를테면 Label 표기법 같은 것들이다.

Stoyan의 문서는 매카시의 리스프 문헌을 중심으로 재구성한 것이다. The Influence of the Designer on the Design -- J.~McCarthy and Lisp(http://www8.informatik.uni-erlangen.de/html/lisp/mcc91.html)라는 제목으로 찾아볼 수 있다.

지난 회에 설명한 리스프의 기본적인 논문은 매카시가 쓴 Recursive Functions of Symbolic Expressions and their Computation by Machine의 Part 1이었다(Part 2는 끝내 나오지 않았다). 이 글은 지금 봐도 참신함을 잃지 않을 정도로 잘 정리된 글이다. 몇 년에 걸쳐 다듬고 매만진 글이기 때문인지도 모른다.

매카시는 이 글과 그 전의 아이디어를 조수에게 보여주었다. 글을 읽던 매카시의 대학원생이었던 스티프 러셀(Steve Russel)은 예상보다 총명했다. 러셀은 eval 함수가 리스프의 인터프리터로 사용될 수 있다는 것을 깨달았다. 결국 리스프를 IBM 704에서 구현했다(나중에 Space War라는 첫 번째 컴퓨터게임을 만들기도 한다).

컴퓨터로 프로그래밍과 디버깅을 거듭하자 무엇인가가 나왔다. 그리고 사람들은 인터프리터로 만든 언어를 갖게 되었다. 갑자기 리스프가 구현된 것이다. 인터프리터가 예상치 못하게 빠르게 나옴에 따라 언어의 형태가 갑작스럽게 초기 상태에서 고정되었고 원래의 논문 ‘Recursive Function...’에서 별생각 없이 만든 결정들이 언어의 요소가 되었다. 그중에는 나중에 별로 좋은 생각이 아니었다는 것으로 밝혀진 것들도 있었으나 대부분은 그대로 살아남았다. 그만큼 리스프는 빠르게 수용되었다. 그리고 매카시 자신이 리스프가 가능한 것이라고 믿었던 것이 아니기 때문에 리스프를 발명한 것이 아니라 발견했다고도 말한다.

매카시는 History of LISP에서 당시 자신이 람다함수에 대해 알기는 했지만 불완전하게 이해하고 있었다는 사실도 인정했다. 처음에 리스프는 람다함수의 불완전한 표기법을 채택하고 있었고 점차 완전한 것으로 변해갔다. 이미 리스프가 만들어진 상태에서 매카시는 몇 가지를 고쳐보려고 했지만 때는 늦었다. 리스프를 실제로 사용하는 사람들과 리스프를 만든 매카시의 생각의 차이가 커져갔고 사용자 그룹의 힘이 더 커져감에 따라 매카시는 언어에 대한 통제를 포기하고 자신의 주제로 돌아가 버렸다. 하지만 리스프를 만든 것은 분명 매카시였다.



위로



리스프 인터프리터의 구현

이번 회의 주제는 ‘Recursive Functions ...’을 폴 그레이엄이 요즘의 리스프로 다시 구성한 것을 설명하는 것이다. 즉 리스프의 재구성이다. 50년이 다 되어가는 옛날 리스프를 다시 구현해 본다는 것이 이상하기는 하지만 인터프리터를 살펴보는 일이 리스프를 이해하는 가장 좋은 방법이며 전통적인 방법이다.

리스프 개발은 사람들이 원시적인 인터프리터를 만들고 그 위에 다른 리스프를 구현하며 발전했다. 사실 리스프의 역사에는 어떤 리스프 인터프리터 위에 다른 리스프가 올라가고 또 다른 것이 그 개량판 위에 올라간 역사가 있다. 얼마나 복잡한 층위를 만들었는지는 아무도 모른다. 이런 방식의 인터프리터를 메타서큘러 인터프리터라고 한다. 처음엔 해커들은 자신의 리스프를 만들었고(라이브러리보다 언어를 만드는 편이 더 빨랐다. 간단했기 때문이다) 이들 중에서 남는 것은 점차 줄어들었다. 아무래도 효과적인 구현들만이 남았기 때문이다. 결국에는 몇 개로 줄어들었다. 하지만 요즘도 리스프 교재에서 메타서큘러 인터프리터를 만드는 일은 중요한 단계로 남아있다. 언어 자체의 이해가 증가하기 때문이다. SICP는 4장 전체를 인터프리터에 바치고 있다.

맨 처음 나온 리스프 인터프리터는 스티브 러셀이 IBM 704에 만든 구현이다. 그 구현은 펀치카드 방식의 IBM 704를 손으로 어셈블하면서 만들어졌고 당시의 자료들 또한 남아있다. 러셀은 나중에 엄청나게 번거로운 작업이었다고 회고했다. 펀치카드로 작업하는 배치 방식 코딩과 디버깅은 쉬운 일이 아니다.

얼마 후 리스프는 DEC의 PDP-1 컴퓨터로 이식되었다. PDP-1의 인터랙티브한 환경에서 리스프가 돌아간다는 것이 알려지자 곧바로 컴퓨터에 빠져 사는 사람들이 나오게 되었다. 이들이 바로 해커들이다. 해커리즘의 근본적인 도구는 매카시의 논문을 스티브 러셀이 작업(해킹)하여 탄생했고 DEC 초기 기계들을 중심으로 MIT에서 해커들이 해킹하는(다듬는) 형태를 통해 발전했다. 스탠포드나 BBN 같은 회사들에서도 중요한 리스프 변종들이 나오기 시작했다.

매카시에게 기계적 지능 구현을 위해 가장 중요한 것은 포멀리즘(형식주의)이었다. 수학의 형식을 빌어 알고리즘으로 문제를 푸는 일은 문제를 기계가 풀 수 있는 형식(표기법)과 기능을 만드는 일이다. 리스프는 중요한 진보였다. 동일하지는 않지만 처치의 람다 계산법을 사용하여 범용의 튜링머신을 만들어 낸 것이기 때문이다. 포멀리즘은 기계가 다룰 수 있는 형태로 표현할 수 있다면 기계가 반복적인 계산을 되풀이하여 정확한 답을 줄 수 있기 때문에 형식과 형식의 체계는 매우 중요하다. 또 이 형식주의는 사람 손으로 문제를 푸는 것보다는 기계에 더 적합한 형태의 연산이라는 것이 매카시의 생각이었다. 정확한 형식으로 표현할 수 있다면 기계가 못 풀 문제도 없다는 것이다.

초기 인공지능은 이렇게 소박한 기반 위에서 출발했다. 물론 기계가 풀 수 있을 정도로 간단한 형식으로 바꾸는 것이 쉽지 않다는 것을 발견하는 과정이 곧 뒤따랐다. 매카시가 차용한 람다 계산법이라는 것은 일종의 일반화된 함수의 치환으로 생각할 수 있다. 그런데 이 간단한 치환으로 많은 것들을 할 수 있다. 기호와 기호 표기의 포멀리즘이 지배하는 곳에서는 더욱 그렇다. 기계적으로 기호를 치환하는 것으로 아주 많은 일을 할 수 있다. 인공지능도 그런 일들 가운데 하나다.

인공지능의 또 다른 개척자 마빈 민스키는 프로그래밍의 다른 측면을 보았다. 앞으로 필요한 인공지능과 관련하여 프로그램들의 개발이 많이 필요할 것이며 이 프로그램들은 컴퓨터에 빠져있는 해커들이 아니면 만들어낼 수 없다는 것을 본 것이다. 민스키는 학자들의 엘리트주의나 권위주의적인 기업의 기술 문화가 아닌 해커 문화의 일면을 보았다. 해커들의 지성의 다른 측면이었다.

민스키는 자유방임적인 놀이터의 주인 역할을 자처했다. MIT의 인공지능 연구소에 투입된 자금과 장비를 이용해 해커들을 고용하고 이들이 마음껏 프로그래밍을 할 수 있는 환경을 만들었다. 해커들은 대학원생 출신이거나 다른 곳에서 들어오기도 했다, 급료는 높지 않았다. 오로지 해킹이라는 일 자체가 목표였고 컴퓨터를 마음대로 쓰는 것으로 동기는 충분히 높았다고 전한다. 이런 것들을 좋아하는 사람들에게 장난감을 던져주고 그들이 원하는 것을 하게 내버려두는 것이 민스키의 아이디어였다. 당시의 인공지능 연구소에는 할 일이 많았다. 이 놀이터에서 해커들은 마법사로 볼 수 있고 착한 놀이터 주인인 민스키가 부탁을 하면 무엇이든지 만들어 주곤 했다.

다만 해커들의 놀이에는 스스로 정한 엄격한 문화와 기준이 있었다. 당시로서는 이런 놀이터는 인공지능 연구소가 유일했다. 이들의 개성과 배경은 모두 달랐다. 이윽고 특이한 문화가 탄생했다. 그 특징의 하나인 강한 개성과 자유, 그리고 이들과 양립하는 고도의 지성이 있었다. 스티븐 레비의 『해커』라는 책은 당시의 분위기를 전한다. 이런 분위기를 유지하는 것이 얼마나 어려운가를 상상하는 것은 오늘날에도 어렵지 않다. 1960년대에는 요즘보다 더 어려운 일이었지만 해커들의 놀이터는 실제로 여러 해 동안 존재했고 고도의 지적 기준과 심미안, 몰입과 창조의 와중에서 프로그램들과 문화가 태동했다. 스티븐 레비에 의하면 이런 일들은 결국은 해커들의 자기 표현이었다. 일종의 창조적 예술이라고 본 것이다.

말이 길어졌지만 그 때 이들이 진지하게 사용했던 언어는 리스프였다. 지금으로 보면 초라한 하드웨어를 가지고 해커들은 이 리스프로 인공지능 연구소에서 원하던 것들을 (거의) 무엇이든지 만들어 주는 마술을 부렸다. 인공지능의 유명한 프로그램들이 빈약한 기계에서 리스프로 만들어졌다. 당시에는 뛰어난 사람들이 리스프에 빠져 있었고 리스프를 바탕으로 만든 언어들도 많으며 리스프에서 많은 영감을 받기도 했다. 리스프는 처음부터 언어라기보다는 수학적 표현이나 알고리즘에 더 가까웠던 것이다.



위로



구현하기 전에 고려할 것들

이번 회의 주제가 매카시의 ‘Recursive Function ...’을 이해하는 것이므로 다시 원래 주제로 돌아가 보자. 지난 글은 7개의 기본 연산자를 만드는 것으로 끝났다. 정말 이 7개의 식으로 인터프리터를 만들 수 있을까? 이것이 이번 주제다. 답은 미리 말했듯이 “만들 수 있다”이다.

문제는 리스프에 접할 기회가 적었기 때문에 관심이 있다고 해도 리스프를 전혀 모르면 설명이 애매하다고 느낄 수 있는 부분이 있어 여기에 대해 약간의 보충 설명이 필요할 수 있다. 보충 설명을 위해 『A Gentle Introduction to Symbolic Computation』(http://www.cs.cmu.edu/~dst/LispBook/index.html)이라는 훌륭한 책이 있다. 책의 앞부분을 읽고 그림을 보고 있으면 보조 자료로 충분하다. 하지만 필자는 가급적 설명을 쉽게 하려고 애쓸 것이다. Peter Siebel의 『Practical Common LISP』(http://www.gigamonkeys.com/book/)도 쉽게 읽을 수 있는 책이다. 이 정도면 역시 충분할 것이다.

그리고 리스프를 실행할 수 있는 적당한 환경이 있어야 한다. 요즘은 LispWorks나 Franz Lisp 같은 곳에서 윈도우와 리눅스용 리스프를 다운로드할 수 있으므로 문제가 될 것이 없다. 그 외에도 많은 리스프 구현이 있으며 소스까지 공개된 것들도 있다. 하지만 이번 설명에서 반드시 리스프가 필요한 것은 아니다. 종이와 연필로도 풀어볼 수 있다.

리스프에서 식(expression)이 리스트일 때 첫 번째 요소가 연산자(operator)이면 나머지 요소들은 인자(argument)로 작용한다. 이를테면 2+3은 (+ 2 3)으로 표시한다. 연산자는 +이고 2와 3은 인자인 것이다. 먼저 지난번에 설명한 7개의 연산자를 다시 적어 보자.

  1. (quote x)는 x를 되돌리며 ‘x와 같다.
  2. (atom x)는 x가 아톰이라는 기본형의 원소이거나 빈 리스트이면 t를, 아니면 ()를 되돌린다(t는 참을 의미하고 ()는 거짓을 의미하는 값이라고 하자).
  3. (eq x y)는 x와 y의 값이 같으면 t를, 아니면 ()를 되돌린다.
  4. (car x)는 리스트 x의 첫 값을 되돌린다.
  5. (cdr x)는 리스트 x의 첫 값을 제외한 나머지 값을 되돌린다.
  6. (cons x y)는 x로 시작하고 리스트 y의 값들이 따라오는 리스트를 돌려준다.
  7. (cond (p1 e1) ... (pn en))은 p1부터 시작하여 p로 시작하는 식이 참이 나올 때까지 계산한다. 만약 pi에서 참이 나오면 해당하는 식 ei를 전체 cond의 값으로 되돌려준다. 끝까지 참이 나오지 않으면 빈 리스트를 되돌린다.

먼저 2번의 atom이라는 연산자를 살펴보자. 리스프에서 어떤 식이 atom이라는 것은 리스트가 아니라는 것을 의미한다. 기호 아톰(atomic symbol)은 어떤 기호가 atom의 성질을 갖는다는 것을 의미한다. 또한 리스프의 S-식(S-Expression)을 다음과 같이 정의한다. 우선 S-식의 표현을 리스프에서 의미를 부여한 기호인 ( ).를 사용하여 나타내기로 하자.

  1. 기호 아톰은 S-식이다.
  2. 만약 e1과 e2가 S-식이라면 (e1 . e2)도 S-식이다.

정의는 A, B, AB와 같은 기호는 당연히 S-식이다. 그러므로 정의 2에 의해 (A . B)도 S-식이며 ((AB . C) . D)도 기호식이다. 그러므로 리스프에서는 기호 아톰과 리스트 두 종류의 S-식 형태만이 존재한다. 따라서 (atom x)가 참이면 x는 리스트가 아닌 S-식, 바로 기호 아톰이다.

이제는 4, 5, 6번을 조금 자세히 살펴보자. 이들은 리스트를 만들고 리스트를 조작하는 핵심적인 기능을 한다. 리스프가 LISt Processing이라는 것을 생각하면 핵심적인 조작이다. 모든 일들은 CONS 셀(Construct Cell)이라는 자료구조를 중심으로 일어난다.

pic1

CONS 셀의 왼쪽은 CAR라고 부르며 오른쪽은 CDR이라고 부른다. 리스트 구조와 리스트로 나타내는 식의 의미는 매카시의 ‘Recursive Function...’를 보아야 할 것이나 여기서는 이것으로 충분하다(초기 IBM 704와 그 후속 기종은 36비트로 15비트씩을 CAR와 CDR에 할당했다. CAR와 CDR은 어셈블러의 매크로 함수 이름이었다고 한다).

가장 기본적인 식은 CONS다. CONS는 두 개의 인자를 취해 이들을 연결한다. 그래서 (cons 1 2)는 (1 . 2)를 리턴한다. 앞의 cons 셀에서 car는 1 이고 cdr은 2이다. 따라서 다음과 같은 식이 가능하다.

  
(car (cons 1 2)) ==> 1
(cdr (cons 1 2)) ==> 2


리스트는 다음과 같이 표현된다. 이를테면 3개의 요소로 구성된 리스트 (1 2 3)이 있다고 하자. 그러면 이 리스트의 실제 모양은 아래 그림과 같다.

pic2

(1 2 3)을 만들고 조작하는 방법은 재귀적이다.

  
(cons 1 (cons 2 (cons 3 nil))) ==>(1 2 3)


위 식의 car를 구하면 다음과 같다.

  
(car  (cons 1 (cons 2 (cons 3 nil))) ) ==>(car (1 2 3))==> 1 


그림에서 상상할 수 있듯이 리스트 (1 2 3)의 car는 1이다. (1 2 3)의 cdr을 구하면 다음과 같다.

  
(cdr  (cons 1 (cons 2 (cons 3 nil))) ) ==> (cons 2 (cons 3 nil)) ==>(2 3)


첫 번째 박스의 CDR이 가리키는 포인터는 2와 3의 리스트인 것이다. 앞 식의 car를 다시 구한다면 다음과 같다.

  
(car (cdr (cons 1 (cons 2 (cons 3 nil))) )) ==> (car (2 3)) ==> 2 


이런 식으로 문제를 해결한다. 함수형 스타일(functional style)이다. 이보다 더 복잡한 것들도 재귀를 이용한 함수형 방식으로 처리할 수 있고 인터프리터가 만들어내는 복잡한 치환도 마찬가지다. 복사도 할 수 있고 리스트를 뒤집을 수도 있다. 리스트 안의 리스트와 같은 중첩된 표현도 가능하다. 위의 car와 cdr의 조합들은 많이 사용되는 것들이라 아래와 같은 표기법으로 사용되기도 한다.

  
(caar list) === (car (car list))
(cadr list) === (car (cdr list))
(cadadr list) === (car (cdr (car (cdr list))))


Root of LISP에 나오는 리스트 예제들도 간단하다.

  
(car '(a b c)) ==>a 
(cdr '(a b c)) ==>(b c) 
(cons 'a '(b c)) ==>(a b c) 
(cons 'a (cons 'b (cons 'c '()))) ==>(a b c) 
(car (cons 'a '(b c))) ==>a
(cdr (cons 'a '(b c))) ==>(b c) 


마찬가지로 다음과 같다.

  
(cadr '((a b) (c d) e)) ==>(c d) 
(caddr '((a b) (c d) e)) ==>e 
(cdar '((a b) (c d) e)) ==>(b) 


하나 더 남아있다. list라는 연산자를 이용하는 것이다. (list e1 ... en)은 결국 (cons e1 ... (cons en '()) ... )과 같은 형식이다. 예를 들면 아래의 두 식은 같은 값을 되돌려준다.

  
(cons 'a (cons 'b (cons 'c '()))) ==>(a b c) 
(list 'a 'b 'c) ==>(a b c) 


아직까지는 특별히 이해에 어려울 것이 없는 것 같다.
7번의 cond도 간단하다. (cond (p1 e1) ... (pn en))은 p라는 술어(predicate)를 차례로 계산하여 참이 나올 때까지 계산하고 참이 나오면 pi에 대한 ei를 계산하여 되돌린다. 만약 참이 나오지 않으면 ‘()를 되돌린다(빈 리스트 ’()를 일단 false로 생각하자). 매카시의 책에서는 (p1 -> e1, ... , pn -> en)으로 표기했다. 예를 들면 (1 < 2 -> 4, 1 > 2 ->3)은 4를 되돌린다.

리스프에서도 다음과 같은 예제를 보면 별다른 것이 없다. 'a와 ‘b가 같지 않으므로 두 번째 술어부를 계산하고 ’a가 atom이므로 second가 나온 것이다. 만약 ‘a가 atom이 아니었으면 두 번째 술어부도 참이 아니므로 끝에 도달하여 ’()를 리턴하였을 것이다.

  
(cond ((eq 'a 'b) 'first) ((atom 'a) 'second)) 
second 


위의 연산자 중에서 quote와 cond를 제외하고 나머지는 먼저 연산자가 계산되고 나서 인자들이 계산된다. 이런 연산자를 함수(function)라고 부른다. 함수 표기법에 대해 매카시의 의견은 매우 간단했다(요즘은 당연히 여겨지는 것이 당시엔 나름대로 중요한 결정이었다).

사람들이 y2+x와 같은 form을 함수와 구별 없이 사용하는 경향이 있는데 알론조 처치는 앞의 식을 form이라고 불렀고 form이 함수가 되려면 인자들의 값이 form에서 어떤 값과 일치하는지 알 수 있어야 한다는 것이다. 처치가 고안한 표기법은 E가 form이라고 할 때  ((x1 ... xn), E)로 표기하면 인수의 차례는 x1에서 xn까지 일치해야 한다는 것이다. 람다는 일단 이런 표기법이라고 할 수 있다.

리스프에서 함수는 (lambda (p1 ... pn ) e)로 표시하며 p 1 ... pn은 인자(parameters)이고 e는 식이다. 함수 호출(function call)의 일반적 형태는 다음과 같다.

  
((lambda (p1... pn ) e) a1 ... an) 


여기서 a1 ... an의 식들을 모두 계산하고 난 후 식 e를 계산한다. e 식을 계산할 때 pi는 해당하는 계산된 ai의 값과 일치한다. a1부터 an까지를 모두 계산하여 적용시키기 때문에 값을 전하는 것이다(call by value). 이름이나 포인터만을 전하는 것과 다르다(call by name). 이 문제는 나중에 다시 논의하게 되며 일단 CBV 방법만을 사용한다고 가정하자. 그러면 모두 계산을 해서 값만을 e에 전달한다는 의미다. 간단한 예를 들면 다음과 같다.

  
((lambda (x y) (+ (* y y) x) 3 4) ==>19 


여기서 y2+x가 인자에 맞추어 계산되었다.

  
((lambda (x) (cons x '(b))) 'a) ==>(a b) 


위 식에서 인자는 'a이고 리스트 ‘(b)와 함께 cons가 적용되었다. 람다를 설명했으니 이제 label을 설명할 차례다.

(label f (lambda (p1 ... pn) e))로 표기하는 것은 함수 (lambda (p1 ... pn) e)로 표기하는 것에 대해 e 안에 f가 나타나는 경우 f는 label 이하의 식으로 계산된다. 이 방식은 N. Rochester가 고안하고 매카시가 채용한 것이다. 처치의 람다로 계산하는 것보다는 간단했다고 한다. 일반적으로 label보다는 defun으로 더 많이 사용한다. 그러니까 (defun f (p1 ... pn) e)라고 쓰는 일이 더 많은 것이다. 람다 함수에 이름이 붙었다고 생각하면 된다.

이제 앞에 나온 식을 바탕으로 몇 개의 함수를 정의해 보자. 여기서 함수 뒤에 점(null이 아니라 null.처럼)을 붙인 것은 파생된 함수를 나타내기 위해서다. 기본적인 7개의 식은 이미 앞에서 설명했다.

  
1. (null. x)는 x가 빈 리스트인지를 검사한다. 
(defun null. (x) 
(eq x '())) 

 (null. 'a) ==>() 
 (null. '()) ==>t
 
2. (and. x y)는 두 인수가 참이면 t를 아니면 ()를 돌려준다. 
(defun and. (x y) 
(cond (x (cond (y 't) ('t '()))) 
('t '()))) 

(and. (atom 'a) (eq 'a 'a)) ==>t 
(and. (atom 'a) (eq 'a 'b)) ==>()
 
3. (not. x) 만약 인수가 ()를 돌려주면 t를, 인수가 t를 돌려주면 ()를 돌려준다. 
(defun not. (x) 
(cond (x '()) 
('t 't))) 

(not (eq 'a 'a)) ==>() 
(not (eq 'a 'b)) ==>t
 
4. (append. x y)는 두 리스트를 취하고 이들을 연결하여 돌려준다. 
(defun append. (x y) 
(cond ((null. x) y) 
('t (cons (car x) (append. (cdr x) y))))) 

(append. '(a b) '(c d)) ==>(a b c d) 
(append. '() '(c d)) ==>(c d) 

5. (pair. x y)는 길이가 같은 두 리스트를 받아 이들로부터 차례로 리스트의 각 
원소를 취한 쌍의 리스트를 돌려준다.  
(defun pair. (x y) 
(cond ((and. (null. x) (null. y)) '()) 
((and. (not. (atom x)) (not. (atom y))) 
(cons (list (car x) (car y)) 
(pair. (cdr x) (cdr y))))))

복잡해 보이지만 실제의 동작은 간단하다.
 
(pair. '(x y z) '(a b c)) ==>((x a) (y b) (z c))
 
6. (assoc. x y)은 아톰 x와 pair로 만든 리스트 y를 받아 쌍의 첫 원소가 x와 
동일한 리스트의 두 번째 원소를 돌려준다. 
(defun assoc. (x y) 
(cond ((eq (caar y) x) (cadar y)) 
('t (assoc. x (cdr y))))) 

(assoc. 'x '((x a) (y b))) ==>a 
(assoc. 'x '((x new) (x a) (y b))) ==>new 


1부터 6은 매카시의 글에 나오는 식들을 실제의 리스프로 변환한 것들이다.



위로



리스프 인터프리터의 핵심, eval

이제 여기까지 왔으니 eval의 소스를 구경할 차례다. 앞에서 말한 것처럼 eval의 소스 코드는 a4 한 페이지 정도 분량에 지나지 않는다. 리스프 인터프리터에서 eval은 핵심 그 자체로 식을 계산(evaluate)하여 결과를 돌려주는 일을 한다. 식에서 ‘eval.’, ‘evcon.’, ‘evlis.’처럼 점이 붙어있는 함수는 기본 연산자를 바탕으로 파생된 함수라는 것을 알려준다.

  
(defun eval. (e a)
  (cond
    ((atom e) (assoc. e a))
    ((atom (car e))
     (cond
       ((eq (car e) 'quote) (cadr e))
       ((eq (car e) 'atom)  (atom   (eval. (cadr e) a)))
       ((eq (car e) 'eq)    (eq     (eval. (cadr e) a)
                                    (eval. (caddr e) a)))
       ((eq (car e) 'car)   (car    (eval. (cadr e) a)))
       ((eq (car e) 'cdr)   (cdr    (eval. (cadr e) a)))
       ((eq (car e) 'cons)  (cons   (eval. (cadr e) a)
                                    (eval. (caddr e) a)))
       ((eq (car e) 'cond)  (evcon. (cdr e) a))
       ('t (eval. (cons (assoc. (car e) a)
                        (cdr e))
                  a))))
    ((eq (caar e) 'label)
     (eval. (cons (caddar e) (cdr e))
            (cons (list. (cadar e) (car e)) a)))
    ((eq (caar e) 'lambda)
     (eval. (caddar e)
            (append. (pair. (cadar e) (evlis. (cdr e) a))
                     a)))))

(defun evcon. (c a)
  (cond ((eval. (caar c) a)
         (eval. (cadar c) a))
        ('t (evcon. (cdr c) a))))

(defun evlis. (m a)
  (cond ((null. m) '())
        ('t (cons (eval.  (car m) a)
                  (evlis. (cdr m) a)))))


이 식을 돌려보기로 하자. eval 함수는 두 개의 인수를 갖는다. eval. (e a)에서 계산하려는 식 e와 함수 호출에서 atom에 부여할 값을 부여하는 a라는 리스트다. a는 환경(environment)이라고도 부른다. 이 환경은 나중에 나오는 environment model과는 조금 다르다. 환경의 값은 pair를 이용하여 쌍으로 만들어 내며 값을 찾기 위해서는 assoc을 이용한다. eval은 4개의 cond 항목으로 이루어져 있다.

- 우선 식 e가 atom인 경우의 eval.의 동작을 보자. 식 e는 그냥 x이고 환경은 ‘((x a) (y b))다. cond는 assoc.을 이용하여 a를 되돌린다.

  
(eval. 'x '((x a) (y b))) ==>a 


- 두 번째는 e가 (a ...)와 같은 형태의 식으로 a는 atom이다, 그리고 이 경우는 앞에서 설명한 7개의 기본 연산자를 모두 사용하는 경우이며 다시 cond로 각 연산자별로 분기한다.

  
(eval. '(eq 'a 'a) '()) ==> t 
(eval. '(cons x '(b c)) 
'((x a) (y b)))  ==>(a b c) 


quote를 제외한 나머지는 모두 다시 eval을 호출하여 인자의 값을 계산한다.

생각해보면 결국 인자를 모두 계산하여 6개의 기본 연산자에 대입하는 것으로 귀착된다. cond는 조금 더 복잡하다. cond를 계산하려면 evcon이라는 다른 함수를 불러야 한다. 이 함수는 재귀적으로 식의 요소를 평가하여 첫 번째 t가 나오면 술어 다음의 식을 계산한다. t가 나오는 술어가 없다면 ‘()를 리턴한다. evcon 함수는 cond 리스트의 처음부터 eval.하여 참이 나오면 그 술어와 쌍이 되는 식을 eval.하여 돌려준다.

  
(eval. '(cond ((atom x) 'atom) ('t 'list)) '((x '(a b)))) ==> list 


그리고 두 번째 절의 마지막은 매개변수처럼 전달된 함수 호출을 다루는 것으로 아톰을 해당 값으로 치환하는 것이다. 이들은 lambda나 label을 이용하며 그 값이 다시 계산된다.

  
(eval. '(f '(b c)) 
'((f (lambda (x) (cons 'a x))))) 
==> 

(eval. '((lambda (x) (cons 'a x)) '(b c)) 
'((f (lambda (x) (cons 'a x))))) ==> (a b c)


위의 식에서 f는 환경에서 발견되어 (lambda (x) (cons 'a x))로 치환되었다.

- 그 다음은 label이다. label의 식은 매우 복잡하지만 label이 하는 일은 결국 환경 a에 label 함수의 이름과 해당 lambda를 더하는 것이다. 그래서 다음과 같다.

  
(eval. '((label firstatom (lambda (x) 
(cond ((atom x) x) 
('t (firstatom (car x)))))) 
y) 
'((y ((a b) (c d))))) 

==>
 
(eval. '((lambda (x) 
(cond ((atom x) x) 
('t (firstatom (car x))))) 
y) 
'((firstatom 
(label firstatom (lambda (x) 
(cond ((atom x) x) 
('t (firstatom (car x))))))) 
(y ((a b) (c d))))) 


결국 이 식은 환경 a에 firstatom의 람다 식을 추가한 것이다(너무 복잡하게 생각하면 안 된다). 결국 계산이 일어나면 a를 리턴한다.

- 그 다음은 lambda다. ((lambda (p1 … pn ) e) a1 … an)은 evlis를 불러서 a1 ... an의 인자들을 계산한다. 그 다음에 이 계산 값 (v1 ... vn)이 a1 ... an과 쌍을 만들게 되어 (a1 v1) ... (an vn)의 리스트가 환경의 앞에 추가되는 형태가 된다.

  
(eval. '((lambda (x y) (cons x (cdr y))) 
'a 
'(b c d)) 
'()) 
==> 
(eval. '(cons x (cdr y)) 
'((x a) (y (b c d)))) 


위의 식에서 x와 y의 값이 쌍으로 주어졌다. lambda의 인자 리스트는 환경변수로 계산되어 바뀐 것이다(대단히 중요한 결론이다. 리스프 인터프리터는 인자 리스트를 계산하여 환경에 보관한다. 그리고 연산자만 남고 인자 리스트는 없어진다). 결국 계산은 (a c d)를 되돌린다.

위의 eval.은 매카시의 글에 나온 식을 그레이엄이 리스프로 번역한 것들이고 필자는 두 개의 문서를 놓고 비교했다. 그레이엄의 프로그램에서 apply가 보이지 않기 때문에 리스프 인터프리터에 대해 배운 독자들은 이상하다고 생각할지 모른다. 그러나 최초의 리스프 인터프리터 구현은 요즘의 인터프리터와 apply와 eval의 순서가 반대다. 글에서 매카시는 apply를 universal function으로 보았다. 매카시의 리스프에서 apply는 각 인자에 대해 quote를 붙이기 위해 사용되었다. 시작이 되고 나면 모두 eval이 처리한다(일반적으로 apply가 적용되는 곳이 위 식에서 evlis.가 적용되는 부분이다).

역사적인 이유로 매카시가 생각한 용법의 apply도 적어본다.

  
apply[f;args] = eval[cons[f;appq[args]];NIL]
appq[m] = [null[m] -> NIL;
       T -> cons[list[QUOTE;car[m]];appq[cdr[m]]]]

eval [ ]
[...
...
]




위로



끝으로

이게 다인가? 다는 아니지만 핵심이라고 말할 수 있다. 요즘의 리스프에서 몇 개 빠진 부분은 있으나 중요한 부분은 모두 망라한다.

메타서큘러 인터프리터는 상당히 중요하므로 매카시 본인이 만든 인터프리터도 있다. 매카시 자신이 만든 "A Micro-Manual for Lisp -- not the whole Truth"라는 글이 이런 내용으로 2페이지짜리 글을 인터넷에서 다운로드할 수 있다.

이번에 설명한 eval.은 SICP의 강의 비디오 7a에서 서스만이 “모든 언어의 커널(The Kernel of Every Language)” 또는 “The Spirit in the computer”이라고 부르는 것이다. 몇 개의 식만 잘 정의하면 일단 돌아갈 수 있는 인터프리터가 나온다는 것, 이것이 바로 비밀이다. 나중에 이르기까지 인터프리터는 이것보다 조금 더 복잡해졌을 뿐이다. 앞의 프로그램에서 빠진 중요한 문제가 몇 개가 있으며 환경변수의 문제 같은 것이 있다. 이들은 SICP에서 모두 설명된다. The Art of Interpreter의 내용은 당연히 반영되었다.

   소셜 북마크

   mar.gar.in mar.gar.in
    digg Digg
    del.icio.us del.icio.us
    Slashdot Slashdot

이렇게 간신히 돌아가기 시작한 언어를 컴퓨터에 입력하고 종이에 식을 적은 후 검증을 하던 것이 1세대 해커들의 일이었다, 하지만 잘 돌아갔다.

리스프에서 모든 것은 리스트다. 프로그램도 데이터도 리스트이며 이것을 처리하는 인터프리터도 리스트이다. 만약 이런 것들을 일일이 손으로 계산한다면 고역이겠으나 다행히 컴퓨터가 있다.

해커와 화가

정신없이 설명하다보니 독자들은 SICP의 4장을 미리 연습한 셈이 되고 말았다. 그리고 리스프가 구현되던 당시의 상황과 리스프라는 언어의 핵심을 한꺼번에 본 셈이다. 기왕 여기까지 왔으니 SICP나 다른 리스프 책을 보아도 좋을 것이다. 만약 리스프의 장점에 대해 조금 더 고무적인 글을 읽고 싶다면 폴 그레이엄의 『해커와 화가』와 같은 책이 있다. 책에서 리스프의 어떤 점이 중요하며 왜 좋은가에 대해 명쾌하게 설명하고 있다.


Trackback 1 Comment 0
2009.02.01 08:09

LISP에 대한 생각들 -첫번째 이야기.



해커 문화의 뿌리를 찾아서 Part 1: 리스프가 탄생하기까지



안윤호안윤호 mindengine@freechal.com

필자는 아마추어 리눅스 커널 해커였으며 최근에 팹(Fab)이라는 책을 번역 출간했다. 컴퓨터의 여러 분야에 관심이 많고 컴퓨터와 문화의 인터페이스에도 관심이 많다.


2007년 4월 3일


'마법사 책'이라 불린 SICP

SICP(Structure and Interpretation of Computer Programs)는 한때 미국 주요 대학들의 컴퓨터학과 필수 과정 중 하나였다. 책은 매우 어려운 편이고 책의 연습문제라는 것들에는 우수한 컴퓨터 프로그래머들이 고민하던 문제들이 포함되어 있다. 그리고 다른 책과는 그 구성이 너무 다르다. 끝부분에 가면 메타 서큘러 실행기를 거쳐 언어의 컴파일러인지, 하드웨어의 컴파일러인지 경계가 애매한 지경에까지 이른다. 학생들이 당혹해 할 것은 틀림없다. SICP는 컴퓨터 교육의 패러다임을 바꾸어 놓았다는 평을 듣기도 한다.



SICP의 표지
그림 1. 마법사 책이라고 하는 SICP의 표지.
그림에 나온 람다(lambda)와 apply-eval 개념은 책을 관통하는 주제다. 책은 http://mitpress.mit.edu/sicp/에 온라인 문서로 공개되어 있다.

그래서 SICP에 대해서는 많은 사람들의 의견이 반반으로 갈린다. 생각하고 배울 점이 많은 책이라는 의견과 배울 것도 없으면서 시간만 낭비하게 하는 괴상한 책이라는 견해다. 한때 이 책을 필수 학점의 교재로 사용하던 학교에서는 학생들이 과정을 따라가는 일 자체에 공포심을 느꼈으며 불평도 대단했다. 시종일관 생각하게 만든다는 것이 공포심을 갖게 하는 요인이었다. 필자 역시 처음에는 책이 너무나 이상하다고 생각해서, 보다가 두 번이나 집어 던진 기억이 있다. 필자는 강의를 들은 것이 아니라 혼자서 공부했기 때문에 책의 배경을 이해하지 못했다. 책에 대한 재평가는 SICP와 관련된 메모들과 문서들, 그리고 문서들의 역사적인 맥락을 조금이나마 알게 되면서부터다. 그리고 컴퓨터에 대해 다시 생각하게 되었다.

책에 대해 불평하던 사람들 가운데 나중에 이 책을 다시 보거나 재평가하는 사람들이 꽤 있다. 그 중에는 조엘 스폴스키(Joel Spolsky)도 있었다. 조엘은 조엘 온 소프트웨어(Joel On the Software)라는 블로그로 유명한 사람이다. 조엘의 글 중에 “Perils on the Java School”은 미국 대학의 컴퓨터 학과가 너무 쉬운 것들만 다루려고 해서 수준 저하가 올 것이라는 내용이었다.
조엘 자신이 나이를 먹었다는 확실한 징조가 “요즘 아이들이” 어려워 보이는 것들을 하려고도, 할 수 있다고도 생각하지 않는다는 현상이라고 했다. 컴퓨터 학과에서 자료구조와 함수형 언어 같은 것을 심도 있게 가르치지 않기 때문에 평이한 학생들만을 만들어 내며, 학생들은 교과 과정이 어렵지 않기 때문에 많은 생각을 하지 않고 졸업한다. 그래서 평범한 프로그램만 만들어 내는 평범한 프로그래머만이 나올 것 같다는 내용이다(예전에는 자료구조나 함수형 언어 같은 것이 너무 어려워 고교 시절까지 스스로 우수하다고 생각했던 학생들이 자신의 능력을 진지하게 생각하고 과를 바꾸는 경우가 있었다는 것이다. 과정 자체가 사람들을 솎아내는 기능이 있었다).

이렇게 어려운 교과 중 하나가 MIT 학부 과정의 SICP다. 앞서 말했듯이 과거에는 주요 대학들의 표준 교재였다. 조엘은 블로그에서 이렇게 말하고 있다.

“코스의 난이도는 경이적이다. 처음 5번 정도의 강의에서 스킴(Scheme)의 대부분을 배우며 그것으로 다른 함수를 입력으로 받는 fixed-point 함수를 배울 준비가 끝난다. 펜실베니아 대학의 CSE121에서 나 자신이 이런 과정과 씨름할 때, 전부는 아니더라도 대부분은 강의를 따라갈 수 없었다. 교재는 너무 어려웠다. 교수님에게 이런 과정은 부당하다는 긴 하소연 편지를 썼다. 학교의 누군가가 내 말(아니면 다른 불평자)을 들은 것이 틀림없다. 왜냐하면 이 과정은 이제 자바로 진행되기 때문이다. … 차라리 이런 불평을 듣지 않았으면 좋았을 것이다. … 이제 전공과목에서 4.0을 받느라 머리를 쓸 필요가 없어진 것이다.”

아마존의 서평은 책의 내용이 좋다('great'와 'excellent' 정도의 표현은 흔하다)는 평으로 도배되어 있다. 책의 내용이 지나치게 어렵다는 것을 불평하면서도 그렇다. 사람들이 지적하는 불만 사항은 현실적인 면에 관한 것이다. 이 책은 실전에는 바로 도움이 되지 않는다. 서평 목록 처음에 나오는 피터 노빅(구글의 검색엔진 품질 책임자로 norvig.com에 가면 리스프에 대한 많은 글들을 볼 수 있다)이나 폴 그레이엄(비아웹의 창업자로 '해커와 화가'라는 책을 썼다. www.paulgraham.com에 가면 역시 리스프에 대한 많은 글이 있다) 같은 사람들은 SICP에 대해 극찬 일색이다. 두 사람은 리스프의 중요한 옹호자이고 리스프 세계에서 SICP는 중요한 교과서다. 두 사람 모두 리스프에 대해 중요한 책들을 출판했지만 SICP는 아주 중요한 책으로 평가된다.

SICP의 문화 코드, 그리고 리스프

사람들이 이렇게 생각한다면 SICP는 나름대로 중요한 의미를 지닌다. 이유는 간단하다. 사람들이 중요하다고 생각하는 그 무엇이 있기 때문이다. 책에는 생각할 내용이 많은 것이다. 고전으로 평가 받는 이유는 책이 담고 있는 내용도 있지만 책을 주제로 담론을 전개할 수 있기 때문이기도 하다. 그렇다면 책이 어려워도 고생해가며 읽을 가치는 충분하다고 볼 수 있다. 물론 아닐 수도 있다.

SICP는 한글판으로도 출판된다. 필자도 부분적으로나마 번역 과정에 참여했다. 한글 번역의 문제점과 장점을 다 같이 갖고 있는 SICP의 한글판은 컴퓨터의 고전이 번역되는 것으로 볼 수 있고 중요한 의미를 갖는다. 아무래도 한글판이 나오면 한 명이라도 더 보게 되어 있다. 이를테면 예전에 Computer Organization and Design 같은 고전이 번역되자 사람들이 더 많이 읽고 더 많은 과정에서 교재로 도입했다.
그러나 SICP를 교재로 사용하고 있는 과정은 아무래도 줄고 있다. 조엘이 말한 것과 같은 맥락이다. 그러나 컴퓨터의 중요한 고전을 독학으로 공부하는 것도 가능하다. 이미 학부를 졸업한 사람들이나 관련이 없는 학과를 나온 경우에는 독학 이외에는 방법이 없다(필자 역시 독학 내지 흥미로 공부하고 이 글을 쓰고 있다).

SICP가 쉽다거나 어렵다거나 하는 문제는 교사와 학생의 문제일 수도 있으며(예상보다 높은 수준의 교사가 필요할 것이라는 것이 개인적 의견이다) 리스프나 스킴 자체의 문제일 수도 있다. 커리큘럼이 너무 어렵고 격렬하다고 하여 HTDP(How to Design Programs)와 같은 교과 과정도 있다. 그러나 분명히 이런 격렬함이 이 책을 다른 책과 다르게 만든 그 무엇이기도 하다. 이상한 주입 과정과 사고 과정을 거치면서 분명히 어떤 변화가 나타난다. 그러나 우수한 선생이나 대가에게 배울 수 없다면 우수한 학생 또는 대가학생이라는 방법도 가능하다. 학생이 목표를 선정하고 이리저리 공부하는 방법을 고안하면 된다. 실패율은 높지만 배우는 것도 많다. 아무튼 리스프나 SICP를 배우고 이해하거나 짜보고 싶은 프로그램들이 있었으므로 해봐야 할 과정이었다.

필자는 이 책을 독학하기 위해 여러 가지 자료를 살펴보아야 했고 약간의 시간을 낼 수 있어서 여러 가지를 구경할 수 있었다. 그래서 이번에 쓰는 글은 아마추어 해커의 관점에서 해커 사상의 원류라고 할 수 있는 리스프와 리스프의 중요한 파생 언어 중 하나인 스킴을 살펴보는 것이다.
필자는 컴퓨터로 먹고 살지 않는다. 진지하기는 하지만 생업은 아니다. 아마추어다. 덕분에 하고 싶은 것들을 하고 있어도 누가 뭐라고 하지 않는다. 적극적으로 보자면 컴퓨터는 필자에게 중요한 문화 탐험의 하나다. 그래서 도대체 SICP라는 책을 만들기 위한 문화코드와 재료가 무엇인지, 그리고 역사(history)와 사람들의 이야기(biography)가 어떤 것인지가 더 중요하다. 이런 날줄과 씨줄로 리스프의 문화 코드가 만들어진 것이기 때문이다. 그래서 필자의 이야기는 완전한 전문가의 이야기도 아니며 그저 진지한 아마추어의 이야기 정도로 파악해주면 좋을 것이다.
그래서 SICP라는 책을 해설하는 것이 아니라 SICP를 만든 자료들과 근거들이 어떠한 맥락에서 어떤 경로를 거쳐 발전해 왔는가를 보는 것이 필자의 접근법이다(이러한 접근법은 시간이 조금 더 든다).

SICP의 그 이전에 리스프라는 더 거대한 덩어리가 있었다. 여기에도 문서들이 보존되어 있다. 필자는 코드만 보고 '아하!' 하고 모든 것을 이해하는 천재가 아니기 때문에 이것저것을 살펴보아야 하고 문서가 있으면 다 이해하지는 못하더라도 읽어보려고 했다. 때로는 코드만 보는 것보다는 더 이해가 빠를 수도 있겠다. 자칫하면 재미가 없을 수도 있으므로 접근방법을 미리 설명하는 것이다. 그러니까 필자의 글은 어려운 SICP를 이해하기 위한 주변 자료들을 제시하고 나름대로 설명해 보고 싶은 것이다. 실제로 리스프나 스킴이 오랜 세월 동안 진화해 왔기 때문에 역사성을 무시하지 못한다. 그래서 역사적 자료와 문헌들을 사람들의 이야기와 몇 줄의 코드에 섞어서 이야기할 수밖에 없다. 교양과목들처럼 말이다.

거슬러 살펴본 리스프의 탄생

SICP는 개정 과정을 몇 차례 거쳤는데 SICP는 리스프가 아니라 리스프의 방언인 스킴으로 되어 있다. 그리고 스킴을 만든 사람은 가이 스틸(Guy Steele)과 제럴드 서스만(Gerald Sussman)이다. 그 전까지의 리스프와 스킴의 차이점이라면 스킴은 칼 휴이트(Carl Hewitt)의 액터 모델(actor model)을 구현하기 위해 만들었으며 그 이전의 다른 리스프 구현의 전통을 이어받았다는 점이다.
그런 노력의 와중에서 전통적인 리스프에 대한 심각한 의문 제기가 있었고 일련의 사고 과정은 람다 페이퍼(lambda paper)라는 이름으로 나타난다. 결국 여러 편의 논문들이 나오고 SICP라는 책으로 만들어지기까지는 10년의 세월이 필요했다. 1970년대 초반부터 작업이 시작되어 1980년대 중반에야 책으로 나온 것이다.



존 매카시
그림 2. 리스프를 발명(또는 발견)했다고 하는 존 매카시

이른바 이들이 발표하는 람다 페이퍼라는 것을 사람들은 좋아하기도 했고 싫어하기도 했다(글들은 http://library.readscheme.org/page1.html에 있다). 그 중 "The Art of the Interpreter of, the Modularity Complex(Parts Zero, One, and Two)"라는 유명한 문서가 많은 사람들에게 영향을 주었다. 그 외에도 "Lambda: The Ultimate Imperative"와 “Lambda: The Ultimate Declarative”, 그리고 “Debunking the 'Expensive Procedure Call' Myth, or, Procedure Call Implementations Considered Harmful, or, Lambda: The Ultimate GOTO”라는 글들도 유명하지만 사람들은 이 글들을 별로 좋아하지 않았던 것 같다. "Lambda: The Ultimate X” and “X considered Harmful"이라고 풍자하는 글들도 있었다고 한다.

중요한 내용이라 언급하지 않을 수 없는 액터 모델에 대해서도 설명하면 좋겠지만 지면상 불가능하다. 일단 위키 백과의 소개 글 정도면 큰 그림을 이해하는 데는 충분할 것이다. 휴이트와 대화하면서 람다가 액터와 같다는 것을 확인한 서스만은 정말로 좋아했다고 한다. 나중에 앨런 케이(Alan Kay)와 휴이트가 만나면서 스몰토크(smalltalk) OOP(Object-Oriented Programming)의 메시지 모델과 액터 모델은 서로 많은 영향을 주고받았다.

1970년대 스킴이 람다 페이퍼를 중심으로 사람들의 관심과 비난을 한데 얻던 시절, 서스만과 함께 스킴에 대한 연구를 진행하던 가이 스틸은 당시를 아주 재미있던 시절이라고 나중에 회고했다. 스틸은 당시 대학원을 다녔다. 석사 과정에 다니는 학생이 젊은 교수와 함께 언어의 중요한 틀을 만든 것이다. 스틸은 나중에 스킴 컴파일러에 대한 논문을 쓰고 D. Hillis의 Thinking Machine으로 자리를 옮겼기 때문에 SICP가 출판될 때는 동료인 Harold Abelson이 공동 저자로 되어 있다(가이 스틸은 현재 썬(Sun)의 연구진으로 있다).
기존의 리스프에 대해 문제점을 제기한 가이 스틸은 나중에 CLTL(Common Lisp The Language)라는 리스프 드래프트의 작성자가 되었다. 드래프트를 만들며 리스프의 많은 구현들의 장단점을 취합했다. 그만큼 실력이 있었다(스틸은 C 표준안과 자바 표준 그리고 포트란의 표준안을 작성했거나 위원회의 주요 멤버이기도 했다).

스킴이 그전까지의 리스프와 중요한 차이를 보인 것은 람다에 대한 중요성을 부각시키고 람다의 행동에 대한 엄밀한 분석을 이룬 것이다. 테일 리커전(tail recursion)이나 렉시컬 스코프(lexical scope)와 같은 것도 중요한 차이점이다. 람다에 대해 생각한 것은 앞의 오리지널 람다 페이퍼라는 문서들이 바로 그 증거이며 SICP에는 이 문서들의 내용이 녹아 들어있다. 진지한 독자들이라면 호기심으로라도 람다 페이퍼들을 살펴볼 필요는 충분히 있겠다.

튜링 머신과 람다 계산법

컴퓨터 역사에서 리스프 초기 해커들은 1세대 해커에 속한다. 바로 스티븐 레비의 “해커”에 나오는 사람들이다. SICP의 저자인 제랄드 서스만 역시 1960년대 중반에 이 문화권 속에 들어와 있었다. 스티브 러셀(Steve Russell)이나 다른 해커들과 같이 구현된 지 얼마 되지 않은 리스프와 PDP 컴퓨터를 가지고 기계들과 하나가 되어 생활했다. 이들에게 있어서 리스프와 MIT의 AI 연구소는 하나의 도약대였다. 그리고 해커 문화를 탄생시켰다. AI 연구소는 자유롭게 프로그래밍을 할 수 있는 장비와 분위기를 제공한 최초의 장소였기 때문이다. 그래서 컴퓨터의 역사에서 리스프의 위치는 매우 중요하다("A Marriage of Convenience: The Founding of the MIT Artificial Intelligence Laboratory")라는 문서가 있다).

리스프가 하나의 중요한 언어가 될 수 있었던 것은 수학적인 아이디어의 표현에서 뛰어났기 때문이다. 그것은 우선적으로 람다와 리커전이었다. 시작은 조금 묘하며 컴퓨터의 시작에도 관련이 있다.

조금 더 역사를 거슬러 올라가면 리스프라는 언어를 만든 매카시가 알론조 처치(Alonzo Church)의 제자였다. 처치는 미국의 수학자이자 논리학자였다. 처치의 제자 중에는 뛰어난 사람이 많았다. 컴퓨터의 시작이라고도 하는 앨런 튜링(Alan Turing)도 처치의 제자였다. Stephen Cole Kleene이나 John George Kemeny도 처치의 제자다.
컴퓨터의 시작인 수학적인 문제를 기계적으로 푸는 문제는 튜링에 의해 튜링 기계로 알려져 있다. 이것은 1936년 튜링이 발표한 “On Computable Numbers, with an Application of Entscheidungs Problem”이라는 애매한 제목의 논문에서 표면으로 나오게 되었다. 튜링은 당시 중요한 수학적 문제였던 entscheidungs problem에 태클을 건 것이다. 이 문제는 수학자 데이비드 힐버트가 1928년 제시한 것이다. 적어도 이론상으로 주어진 수학적 주장이 증명 가능한 것인지 판단할 수 있는 명확한 프로시저가 있는지에 대한 문제다. 이런 문제는 튜링과 같은 사람을 위한 문제로 증명의 답은 “그렇지 않다”는 것이었다. 처치 역시 같은 결론에 도달한 논문을 1936년에 발표했다. 프로시저의 기계화에 대해 눈이 뜨인 것이다.

프로시저라는 의미를 더 명백하게 하기 위해 튜링은 LCM(Logical Computing Machine)이라고 부르는 추상적인 기계를 발명했다. LCM은(다른 사람들은 튜링 머신이라고 불렀다) 명령과 데이터를 담은 종이테이프를 갖고 있고 테이프를 따라 움직이는 헤드가 정해진 규칙에 따라 명령을 읽고 해석하며 테이프에 새로운 것을 기록할 수 있는 기계다. 이런 종류의 기계는 진술한 내용의 진위를 테스트하는 프로시저를 따라할 수 있다. 같은 시기 프린스턴 대학에는 폰노이만도 있었다.
문제를 푸는 기계에 대한 튜링의 생각은 자연스럽게 일반적인 컴퓨팅 기계를 만드는 쪽으로 흘렀다. 튜링은 “Proposal for Deveolpment in the Mathematical Division of an Automatic Computing Engine”이라는 제안서를 영국의 국립 물리학 연구소(NPL)에 제출했다. 그 후 에니악(ENIAC)이 나오고 다시 폰노이만에 의해 프로그램 가능한 전자식 디지털 계산기가 나왔다.

그러나 처치가 같은 문제에 동원한 수단은 람다 계산법(lambda calculus)이었다. 람다 계산법 자체가 계산 가능한 함수를 다루는 것이므로 그 자체를 가장 단순한 범용 컴퓨터라고 생각할 수 있고 람다 계산법이 나온 지 20여 년이 지난 후에 매카시는 람다 계산법을 새로운 컴퓨터 언어에 도입할 것을 고려했다. 튜링 머신이 컴퓨터라는 기계에 영향을 주었다면 람다 계산법은 컴퓨터 언어에 영향을 주었다고 볼 수도 있다.
매카시의 유명한 글 "Recursive Functions of Symbolic Expressions and Their Computation by Machine"은 1960년 4월에 작성되었다. 그 이전에는 몇 개의 메모들과 편지들이 남아있다. 이 글이 바로 리스프의 시작이라고 봐도 좋다.

매카시의 아이디어는 MIT의 AI 연구소에서 허망할 정도로 빨리 하나의 프로그래밍 언어로 만들어졌다. 처음에는 종이에 적던 핸드 컴파일 수준의 작업이 하나의 아이디어로 만들어지고 그 다음에는 스티브 러셀에 의해 실제로 컴퓨터 프로그램으로 만들어진 것이다. 리스프는 이렇게 갑자기 만들어진 것이다.
람다 함수를 이야기하면 독자들이 질릴 것 같아서 실제로 리스프가 어떻게 구현되었는가를 보여주는 것이 재미있을 것 같다. 실제로는 SICP 4장에 나오는 내용을 아주 간단하게 미리 설명해보자는 것이다.

그렇다면 매카시가 만들었다는 리스프라는 것이 도대체 무엇인가. 매카시가 사용했다는 IBM 704가 없으니 오늘날의 PC로 만들어 보는 수밖에 없다. 실제로 폴 그레이엄이 이런 실험을 했다. 폴 그레이엄이 책에 수록하지는 않았으나 인터넷에서 볼 수 있는 The Roots of Lisp라는 글은 매카시가 쓴 논문을 오늘날 우리가 사용하는 커먼 리스프(Common LISP)로 구현한 것이다. 그것은 바로 원시적인 인터프리터가 얼마나 쉽게 구현될 수 있는지를 보여주는 것이다. 인쇄하면 A4 한 쪽도 안 되는 소스코드로 리스프 인터프리터가 구현될 수 있다(물론 기계어로 구현하는 것은 더 복잡하지만 불가능할 것도 없다. 파이썬이나 자바로도 리스프 인터프리터를 작성할 수 있다).

원시적인 인터프리터를 만드는 것은 정말 쉽다. 여기서 조금씩 덧붙이면 SICP의 인터프리터가 된다. 메타 서큘러 인터프리터라고 하는 것으로 리스프가 수행되는 기계가 있다면 리스프를 돌려보는 인터프리터로 귀착된다. 그리고 이 코드는 빈약하기 그지없던 1960년대의 하드웨어로도 잘 수행되던 코드다. 이 간단한 코드가 코드를 만들고 그 코드가 코드를 또 만들어낸 것이다. 인공지능의 복잡한 코드들도 간단한 인터프리터에서 시작된 것이다.
이 인터프리터의 모든 식은 일곱 개의 간단한 원시 연산자로 해결할 수 있다. 리스프의 식을 리스트로 표현하면 가장 먼저 나오는 식은 연산자(operator)이며 다른 요소는 인수(arguments) 또는 피연산자(operands)라고 생각할 수 있다. 연산자로 quote, atom, eq, car, cdr, cons, cond를 사용할 수 있으면 원리적으로 리스프 인터프리터를 만들 수 있다.

   소셜 북마크

   mar.gar.in mar.gar.in
    digg Digg
    del.icio.us del.icio.us
    Slashdot Slashdot

  1. (quote x)는 x를 되돌리며 ‘x와 같다.
  2. (atom x)는 x가 아톰이라는 기본형의 원소이거나 빈 리스트이면 t를, 아니면 ()를 되돌린다(t는 참을 의미하고 ()는 거짓을 의미하는 값이라고 하자).
  3. (eq x y)는 x와 y의 값이 같으면 t를, 아니면 ()를 되돌린다.
  4. (car x)는 리스트 x의 첫 값을 되돌린다.
  5. (cdr x)는 리스트 x의 첫 값을 제외한 나머지 값을 되돌린다.
  6. (cons x y)는 x로 시작하고 리스트 y의 값들이 따라오는 리스트를 돌려준다.
  7. (cond (p1 e1) ... (pn en)은) p1부터 시작하여 p로 시작하는 식이 참이 나올 때까지 계산한다.
  8. 만약 참이 나오면 해당하는 식 e를 전체 cond의 값으로 되돌려준다.

이게 다인가? 다는 아니지만 이 연산자들만으로 인터프리터를 만들 수 있다. 다음 회에는 앞의 식들에 간단한 설명을 붙이고 진짜 인터프리터를 만들어 수행을 시켜보겠다.





Trackback 0 Comment 0