Lisp의 성공과 한계, 그리고 더 나은 미래를 위한 방향을 살펴본 Richard P. Gabriel의 글.
Lisp: 좋은 소식 나쁜 소식 크게 이기는 법 Richard P. Gabriel Lucid, Inc 1991년 최초 출간.
초록
Lisp는 지난 10년 동안 상당히 잘해 왔다. 거의 표준화되었고, 상업 부문의 토대가 되었으며, 뛰어난 성능을 달성했고, 좋은 환경을 갖추었으며, 애플리케이션을 배포할 수 있게 되었다. 그러나 Lisp 공동체는 할 수 있었던 만큼 잘하지는 못했다. 이 글에서는 성공, 실패, 그리고 다음에 할 일을 살펴본다.
Lisp 세계는 매우 좋은 상태다. 10년 전에는 표준 Lisp가 없었다. 가장 표준에 가까운 Lisp는 PDP-10과 Xerox Lisp 머신에서 실행되던 InterLisp였다(Vax에서도 실행된다고 하는 사람들도 있었지만, 나는 과장이라고 생각한다). 두 번째로 표준에 가까운 Lisp는 PDP-10에서만 실행되었지만 그 머신의 가장 널리 쓰인 세 운영 체제에서 실행되던 MacLisp였다. 세 번째는 많은 머신에서 실행되었으나 사용하려는 사람이 거의 없던 Portable Standard Lisp였다. 네 번째는 두 종류의 Lisp 머신에서 실행되던 Zetalisp였다. 다섯 번째는 몇 가지 서로 다른 종류의 머신에서 실행되었으나 사용하려는 사람이 거의 없던 Scheme였다. 오늘날의 기준으로 보면, 이들 각각은 성능이 나빴거나 겨우 수용할 만했고, 환경은 없었거나 겨우 만족스러운 수준이었으며, 다른 언어 및 소프트웨어와의 통합은 없었거나 나빴고, 이식성, 수용성, 상업적 전망도 나빴다.
오늘날에는 모든 주요 머신, 모든 주요 운영 체제, 사실상 모든 국가에서 실행되는 Common Lisp (CL)가 있다. Common Lisp는 ANSI 표준화 직전에 있으며, 좋은 성능을 갖추고, 좋은 환경에 둘러싸여 있고, 다른 언어 및 소프트웨어와도 잘 통합된다.
하지만 사업으로서 Lisp는 건강하지 못한 것으로 여겨진다. 실용적인 애플리케이션을 배포하는 수단으로서 Lisp가 버려지고 있다는 끈질기고 때로는 사실인 소문이 있다.
어느 정도 문제는 인식의 문제다. 일반적으로 존재한다고 믿어지는 것보다 더 나은 Lisp 배포 해법들이 분명히 있으며, 불안할 정도로 상당한 부분의 문제는 배치되지 않았거나 잘못 배치된 자원, 착수되지 않은 프로젝트, 가동되지 않은 구현 전략에 있다.
문제의 일부는 인공지능 (AI) 업계에 있는 우리의 매우 소중한 친구들에게서 비롯된다. AI에는 인간 지식과 문제 해결 행동을 형식화하는 훌륭한 접근법들이 여럿 있다. 그러나 AI는 적용 가능한 어떤 영역에서도 만병통치약을 제공하지 않는다. AI를 상업 세계에 초기에 홍보한 일부 사람들은 기대 수준을 너무 높게 올렸다. 이 기대는 전문가 시스템 기반 애플리케이션의 효과성과 배포 가능성에 관한 것이었다.
그 기대가 충족되지 않자, 일부는 희생양을 찾았고, 특히 배포 가능성 문제가 되면 그 대상은 흔히 Lisp 회사들이었다. 물론 AI 회사들이 시장이 결국 배포된 AI 소프트웨어에 무엇을 기대할지 조금이라도 알고 있었다면, 내가 아는 어떤 Lisp 회사와도 그것을 공유한 적이 없다. AI 회사들의 태도는 Lisp 회사들은 살아남기 위해 필요한 일을 할 테니, 고객 목록과 정보를 왜 그들과 공유하느냐는 것이었다고 나는 믿는다.
문제의 또 다른 부분은 Lisp가 받은 비교적 나쁜 언론 보도이며, 때로는 아주 존경받는 출판물에서 나온 것이었다. 나는 Forbes (1989년 10월 16일)에 Julie Pitta가 쓴 Where Lisp Slipped라는 제목의 기사를 보았다. 그러나 그 기사는 Symbolics와 그 회사의 운세에 관한 것이었다. 기사에서 Symbolics에 대한 가장 큰 비판은 AI가 이륙할 것이라고 믿었다는 점, 그리고 독점 하드웨어가 AI의 길이라고 보는 관점을 잘못 밀어붙였다는 점이다. 기사에서 Lisp에 관해 나온 것은 그것이 인공지능에서 광범위하게 사용되는 다소 난해한 프로그래밍 언어라는 진술뿐이었다.
Julie가 Lisp라는 이름으로 자기 기사에 재치 있는 제목을 만들 수 있다고 생각했다는 이유 때문에 Lisp 사업이 부분적으로 타격을 받는다는 것은 유감스럽다.
하지만 Lisp에는 실제 성공도 있고, 문제도 있으며, 그 문제들에서 벗어날 길도 있다.
앞서 말했듯 Lisp는 오늘날 그 어느 때보다 좋은 상태에 있다. Lisp의 몇 가지 성공 사례를 검토하고 싶다.
큰 성공은 표준 Lisp, 즉 Common Lisp가 있다는 점이다. 오늘날 많은 관찰자는 표준화할 수 있는 더 단순하고, 더 작고, 더 깔끔한 Lisp가 있었으면 한다. 그러나 오늘날 우리가 가진, 표준화 준비가 된 Lisp는 Common Lisp다. 그렇다고 나중에 더 나은 Lisp를 표준화할 수 없다는 말은 아니며, 분명 그래야 한다. 더구나 어떤 언어와 마찬가지로 Common Lisp는 필요가 변함에 따라 개선되고 변경되어야 한다.
Common Lisp는 Lisp의 미래를 결정하기 위해 SRI에서 열린 ARPA 후원 회의 뒤인 1981년에 풀뿌리 노력으로 시작되었다. 당시 미국에는 전 MIT 출신 인사들이 정의하고 구현하던 여러 Lisp가 있었다. Greenblatt (LMI), Moon과 Weinreb (Symbolics), Fahlman과 Steele (CMU), White (MIT), Gabriel과 Steele (LLNL)가 그들이었다. Common Lisp 위원회의 핵심은 이 집단에서 나왔다. 그 핵심은 Fahlman, Gabriel, Moon, Steele, Weinreb였으며, Common Lisp는 이 사람들이 관심을 둔 Lisp들이 합쳐진 것이었다.
Common Lisp에 융합될 수 있었던 다른 Lisp도 있었지만, 이들은 MacLisp 전통에 그다지 뚜렷하게 속하지 않았고, 풀뿌리 노력으로 정의된 공통 Lisp보다 자신들의 방언이 성공할 것이라고 예측했기 때문에 이 노력에 적극 참여하기를 거절했다. 이 Lisp들에는 Scheme, Interlisp, Franz Lisp, Portable Standard Lisp, Lisp370이 있었다.
미국 밖에도 Cambridge Lisp와 Le-Lisp를 포함한 주요 Lisp 노력이 있었다. 겸손한 미국의 풀뿌리 노력은 미국 밖의 참여자를 찾지 않았고, 이는 안전하게 실수였다고 볼 수 있다. 솔직히 말해 Common Lisp 집단에는 순전히 미국적인 이 노력이 미국 밖에서도 관심을 끌 것이라는 생각이 전혀 들지 않았다. 집단 구성원 중 AI의 미래가 표준 Lisp의 필요를 북아메리카 너머로 확장할 것이라고 본 사람이 거의 없었기 때문이다.
Common Lisp가 정의되었고, 1984년에 /Common Lisp: the Language/ (CLtL)라는 책이 출간되었다. 그리고 Lisp 머신 회사들과 경쟁하기 위해 일반 하드웨어에 Common Lisp를 올리는 여러 회사가 생겨났다. 4년 안에 사실상 모든 주요 컴퓨터 회사가 자체 구현했거나 Common Lisp 회사에서 상표를 붙여 들여온 Common Lisp를 갖게 되었다.
1986년에는 ANSI 버전의 Common Lisp를 만들기 위해 X3J13이 구성되었다. 그때쯤에는 모호성과 누락을 정리하고, 조건 시스템을 추가하고, 객체 지향 확장을 정의하기 위해 Common Lisp에 중요한 변경이 필요하다는 것이 분명했다.
몇 해가 지나자 좋은 정의를 지닌 성숙한 언어가 있더라도 표준화 과정은 단순하지 않다는 것이 명확해졌다. Common Lisp Object System (CLOS)의 명세만 해도 거의 2년과 X3J13의 가장 재능 있는 구성원 7명이 필요했다.
국제 Lisp 표준화에 대한 관심이 커지고 있다는 점도 분명해졌다. 그러나 Common Lisp의 명백한 후계자는 없었다. Common Lisp의 비판자들, 특히 미국 밖의 비판자들은 실용적 배포 수단으로서 Common Lisp의 실패에 초점을 맞췄다.
1988년에는 Lisp 표준화를 위한 국제 작업반이 만들어졌다. 그 집단은 WG16이라고 불린다. 두 가지는 절대적으로 명확하다. 단기 표준 Lisp는 Common Lisp다. Common Lisp를 넘어서는 장기 표준도 바람직하다.
1988년에는 Scheme을 위한 IEEE 및 어쩌면 ANSI 표준을 만들기 위해 IEEE Scheme 작업반이 구성되었다. 이 집단은 1990년에 작업을 마쳤고, 비교적 작고 깔끔한 Scheme은 표준이 되었다.
현재 X3J13은 ANSI Common Lisp 초안 표준까지 1년도 채 남지 않았다. WG16은 국제적 다툼 때문에 멈춰 있다. Scheme은 IEEE에 의해 표준화되었지만 상업적 관심은 제한적이다.
Common Lisp는 국제적으로 사용되고 있으며, 언제나 논쟁적인 Lisp 공동체가 함께 일하기로 합의할 때까지 적어도 사실상의 표준 역할을 한다.
Common Lisp는 성능이 좋다. 대부분의 현재 구현은 현대 컴파일러 기술을 사용한다. 이는 당시에도 아주 원시적인 컴파일러 기법을 사용했던 오래된 Lisp와 대조된다. 성능 면에서, 오늘날 거의 어떤 컴퓨터에서든 Common Lisp를 사용하는 사람은 1980년대 중반의 단일 사용자 PDP-10이나 단일 사용자 Lisp 머신에서 얻을 수 있었던 것보다 더 나은 성능을 기대할 수 있다. 많은 Common Lisp 구현에는 멀티태스킹과 비침입적 가비지 컬렉션이 있다. 둘 다 10년 전에는 일반 하드웨어에서 불가능한 기능으로 여겨졌다.
사실 Common Lisp는 벤치마크에서 C와 비교해도 성능이 좋다. 다음 표는 세 벤치마크에 대해 Lisp 시간과 코드 크기를 C 시간과 코드 크기로 나눈 비율을 보여 준다.
CPU 시간
코드 크기
Tak 0.90 1.21
Traverse 0.98 1.35
Lexer 1.07 1.48
Tak은 함수 호출과 fixnum 산술을 측정하는 Gabriel 벤치마크다. Traverse는 구조 생성과 접근을 측정하는 Gabriel 벤치마크다. Lexer는 C 컴파일러의 토크나이저이며 디스패치와 문자 조작을 측정한다.
이 벤치마크들은 완전 최적화를 사용하는 표준 Sun C 컴파일러로 1987년 Sun 3에서 실행되었다. Lisp는 비침입적 가비지 컬렉터를 실행하고 있지 않았다.
현대 프로그래밍 환경은 Lisp와 AI 전통에서 나왔다고 주장할 수 있다. 최초의 비트맵 단말기 (Stanford/MIT), 마우스 포인팅 장치 (SRI), 전체 화면 텍스트 편집기 (Stanford/MIT), 창 기반 환경 (Xerox PARC)은 모두 AI 연구에 종사한 연구소에서 나왔다. 오늘날에도 Symbolics 프로그래밍 환경이 최첨단을 대표한다고 주장할 수 있다.
다음 개발 환경 기능들도 Lisp 세계에서 유래했다고 주장할 수 있다.
점진적 컴파일과 적재 기호 디버거 데이터 검사기 소스 코드 수준 한 단계 실행 내장 연산자 도움말 창 기반 디버깅 기호 스택 역추적 구조 편집기
오늘날의 Lisp 환경은 1970년대 최고의 Lisp 머신 환경과 동등하다. 창 기능, 정교한 편집, 좋은 디버깅은 모두 흔하다. 일부 Lisp 시스템에서는 소스 제어 기능, 자동 상호 참조, 자동 테스트를 통해 소프트웨어 생명주기에 상당한 주의를 기울였다.
오늘날 Lisp 코드는 C, Pascal, Fortran 등과 공존할 수 있다. Lisp에서 이 언어들을 호출할 수 있고, 일반적으로 이 언어들은 다시 Lisp를 호출할 수 있다. 이러한 인터페이스는 프로그래머가 Lisp 데이터를 외부 코드에 넘기고, 외부 데이터를 Lisp 코드에 넘기며, Lisp 코드에서 외부 데이터를 조작하고, 외부 코드에서 Lisp 데이터를 조작하고, 외부 프로그램을 동적으로 적재하며, 외부 함수와 Lisp 함수를 자유롭게 섞을 수 있게 한다.
이 기능을 위한 설비는 상당히 광범위하며, 한 번에 여러 다른 언어를 섞을 수단을 제공한다.
Lisp는 어떤 언어보다도 강력하고 포괄적이며 널리 퍼진 객체 지향 확장을 갖고 있다. CLOS는 다른 어떤 객체 지향 언어에서도 찾을 수 없는 기능을 구현한다. 여기에는 다음이 포함된다.
다중 상속 다중 메서드를 포함하는 제네릭 함수 일급 클래스 일급 제네릭 함수 메타클래스 메서드 조합 초기화 프로토콜 메타객체 프로토콜 Lisp 타입과의 통합
Common Lisp (CLOS 포함)는 최초의 표준화된 객체 지향 프로그래밍 언어가 될 가능성이 크다.
Lisp로 작성된 애플리케이션을 배포할 수 있다. 현재 이용 가능한 도구는 좋지만 아직 이상적이지는 않다. 이러한 해법에는 애플리케이션에서 사용하지 않는 코드와 데이터를 제거하는 것, 필요한 코드와 데이터만으로 애플리케이션을 구성하는 것, Lisp 코드에서 .o 파일을 만드는 것이 포함된다.
배포 도구는 Lucid, Franz, Ibuki가 상업적으로 제공한다.
한 마음이 울기에는 눈물이 너무 많아. 한 마음이 계속하기에는 눈물이 너무 많아. 네가 나를 떠난 뒤로, 이제 너는 한참 위에 있고, 언제나 한참 아래의 나를 비웃지.
? & The Mysterians
하지만 이 행복한 이야기에는 슬픈 막간이 있다. AI가 날아오르지 못한 실패 탓으로 돌릴 수도 있는 막간이지만, 우리가 유념해야 할 다른 진실의 알갱이도 아마 들어 있을 것이다. 오늘날 Lisp의 핵심 문제는 서로 대립하는 두 소프트웨어 철학 사이의 긴장에서 비롯된다. 두 철학은 올바른 것과 더 나쁜 것이 더 낫다라고 불린다.
나와 Common Lisp 및 CLOS의 거의 모든 설계자는 MIT/Stanford식 설계에 극도로 많이 노출되었다. 이 스타일의 본질은 올바른 것이라는 구절로 포착할 수 있다. 그러한 설계자에게는 다음 특성을 모두 올바르게 갖추는 것이 중요하다.
단순성 -- 설계는 구현과 인터페이스 모두에서 단순해야 한다. 구현의 단순성보다 인터페이스의 단순성이 더 중요하다. 정확성 -- 설계는 관찰 가능한 모든 측면에서 정확해야 한다. 부정확성은 전혀 허용되지 않는다. 일관성 -- 설계는 일관되지 않아서는 안 된다. 불일관성을 피하기 위해 설계가 약간 덜 단순하고 덜 완전해지는 것은 허용된다. 일관성은 정확성만큼 중요하다. 완전성 -- 설계는 실용적인 범위에서 가능한 한 많은 중요한 상황을 다뤄야 한다. 합리적으로 예상되는 모든 경우를 다뤄야 한다. 단순성 때문에 완전성이 지나치게 줄어드는 것은 허용되지 않는다.
대부분의 사람이 이것들이 좋은 특성이라는 데 동의하리라 믿는다. 나는 이 설계 철학의 사용을 MIT 접근법이라 부르겠다. Common Lisp (CLOS 포함)와 Scheme은 설계 및 구현에 대한 MIT 접근법을 대표한다.
더 나쁜 것이 더 낫다 철학은 약간만 다르다.
단순성 -- 설계는 구현과 인터페이스 모두에서 단순해야 한다. 인터페이스의 단순성보다 구현의 단순성이 더 중요하다. 단순성은 설계에서 가장 중요한 고려 사항이다. 정확성 -- 설계는 관찰 가능한 모든 측면에서 정확해야 한다. 정확한 것보다 단순한 것이 약간 더 낫다. 일관성 -- 설계는 지나치게 불일관적이어서는 안 된다. 어떤 경우에는 단순성을 위해 일관성을 희생할 수 있지만, 구현 복잡성이나 불일관성을 도입하는 것보다 덜 흔한 상황을 다루는 설계 부분을 빼는 편이 낫다. 완전성 -- 설계는 실용적인 범위에서 가능한 한 많은 중요한 상황을 다뤄야 한다. 합리적으로 예상되는 모든 경우를 다뤄야 한다. 다른 어떤 품질을 위해서라도 완전성은 희생될 수 있다. 사실 구현 단순성이 위태로워질 때마다 완전성은 희생되어야 한다. 단순성이 유지된다면 완전성을 이루기 위해 일관성을 희생할 수 있다. 특히 인터페이스의 일관성은 쓸모가 없다.
초기 Unix와 C는 이 설계 학파를 사용한 사례이며, 나는 이 설계 전략의 사용을 New Jersey 접근법이라 부르겠다. 나는 여러분에게 이것이 명백히 나쁜 철학이고 New Jersey 접근법이 나쁜 접근법이라고 설득하기 위해 더 나쁜 것이 더 낫다 철학을 의도적으로 희화화했다.
그러나 나는 허수아비 형태에서조차 더 나쁜 것이 더 낫다가 올바른 것보다 더 나은 생존 특성을 지니며, 소프트웨어에 사용할 때 New Jersey 접근법이 MIT 접근법보다 더 낫다고 믿는다.
먼저 MIT/New Jersey 구분이 타당하고 각 철학의 지지자가 실제로 자신의 철학이 더 낫다고 믿는다는 점을 보여 주는 이야기를 다시 들려주겠다.
한 명은 MIT 출신이고 다른 한 명은 Berkeley 출신이지만 Unix를 작업하던 두 유명 인사가 운영 체제 문제를 논의하기 위해 만난 적이 있다. MIT 출신 인물은 ITS (MIT AI Lab 운영 체제)에 정통했고 Unix 소스를 읽고 있었다. 그는 Unix가 PC loser-ing 문제를 어떻게 해결하는지 관심이 있었다. PC loser-ing 문제는 사용자 프로그램이 IO 버퍼처럼 상당한 상태를 가질 수 있는 긴 작업을 수행하기 위해 시스템 루틴을 호출할 때 발생한다. 작업 중 인터럽트가 발생하면 사용자 프로그램의 상태를 저장해야 한다. 시스템 루틴 호출은 대개 단일 명령어이므로 사용자 프로그램의 PC는 프로세스 상태를 충분히 포착하지 못한다. 시스템 루틴은 되돌리거나 계속 밀고 나가야 한다. 올바른 것은 되돌리고 사용자 프로그램 PC를 시스템 루틴을 호출한 명령어로 복원하는 것이다. 그러면 예를 들어 인터럽트 뒤 사용자 프로그램을 재개할 때 시스템 루틴에 다시 들어간다. 이를 PC loser-ing이라고 하는 이유는 PC를 loser 모드로 강제하기 때문이며, loser는 MIT에서 사용자를 친근하게 부르는 이름이다.
MIT 사람은 이 경우를 처리하는 코드를 보지 못했고 New Jersey 사람에게 문제가 어떻게 처리되는지 물었다. New Jersey 사람은 Unix 사람들이 문제를 알고 있었지만 해법은 시스템 루틴이 언제나 끝까지 완료하게 하는 것이라고 말했다. 다만 때로는 시스템 루틴이 동작을 완료하지 못했음을 알리는 오류 코드가 반환된다. 따라서 올바른 사용자 프로그램은 시스템 루틴을 다시 시도할지 판단하려고 오류 코드를 점검해야 한다. MIT 사람은 이것이 올바른 것이 아니므로 이 해법을 좋아하지 않았다.
New Jersey 사람은 Unix의 설계 철학은 단순성이며 올바른 것은 너무 복잡하기 때문에 Unix 해법이 옳다고 말했다. 게다가 프로그래머는 이 추가 검사와 루프를 쉽게 넣을 수 있다. MIT 사람은 구현은 단순하지만 기능에 대한 인터페이스는 복잡하다고 지적했다. New Jersey 사람은 Unix에서 올바른 절충이 선택되었다고, 즉 인터페이스 단순성보다 구현 단순성이 더 중요하다고 말했다.
그러자 MIT 사람은 때로는 부드러운 닭을 만들려면 터프한 남자가 필요하다고 중얼거렸지만, New Jersey 사람은 이해하지 못했다(나도 이해하는지 확신하지 못하겠다).
이제 더 나쁜 것이 더 낫다가 더 낫다고 주장하고자 한다. C는 Unix를 작성하기 위해 설계된 프로그래밍 언어이며, New Jersey 접근법으로 설계되었다. 따라서 C는 괜찮은 컴파일러를 쉽게 작성할 수 있는 언어이고, 프로그래머에게 컴파일러가 해석하기 쉬운 텍스트를 작성하도록 요구한다. 어떤 이들은 C를 화려한 어셈블리 언어라고 불렀다. 초기 Unix와 C 컴파일러는 모두 단순한 구조였고, 이식하기 쉬우며, 실행에 적은 머신 자원을 필요로 하고, 운영 체제와 프로그래밍 언어에서 원하는 것의 약 50%-80%를 제공한다.
어느 시점에 존재하는 컴퓨터의 절반은 중앙값보다 못하다(더 작거나 더 느리다). Unix와 C는 그들에서 잘 작동한다. 더 나쁜 것이 더 낫다 철학은 구현 단순성이 최우선이라는 뜻이며, 이는 Unix와 C가 그런 머신에 쉽게 이식된다는 뜻이다. 따라서 Unix와 C가 지원하는 50% 기능이 만족스럽다면, 그것들이 어디에나 나타나기 시작하리라고 예상할 수 있다. 그리고 실제로 그랬다.
Unix와 C는 궁극의 컴퓨터 바이러스다.
더 나쁜 것이 더 낫다 철학의 추가 이점은 프로그래머가 좋은 성능과 적당한 자원 사용을 얻기 위해 일부 안전성, 편의성, 번거로움을 희생하도록 길들여진다는 점이다. New Jersey 접근법으로 작성된 프로그램은 작은 머신과 큰 머신 모두에서 잘 작동하며, 바이러스 위에서 작성되었기 때문에 코드는 이식 가능할 것이다.
초기 바이러스는 기본적으로 좋아야 한다는 점을 기억하는 것이 중요하다. 그렇다면 이식 가능한 한 바이러스적 확산은 보장된다. 바이러스가 퍼지면, 기능을 90%에 가깝게 늘리는 식으로 개선하려는 압력이 있을 것이다. 그러나 사용자는 이미 올바른 것보다 못한 것을 받아들이도록 길들여졌다. 따라서 더 나쁜 것이 더 낫다 소프트웨어는 첫째 수용성을 얻고, 둘째 사용자에게 더 적은 것을 기대하도록 길들이며, 셋째 거의 올바른 것이 되는 지점까지 개선될 것이다. 구체적으로 말하면, 1987년에 Lisp 컴파일러가 C 컴파일러만큼 좋았음에도 Lisp 컴파일러를 개선하려는 사람보다 C 컴파일러를 개선하려는 컴파일러 전문가가 훨씬 많다.
좋은 소식은 1995년에 좋은 운영 체제와 프로그래밍 언어를 갖게 된다는 것이다. 나쁜 소식은 그것들이 Unix와 C++라는 것이다.
더 나쁜 것이 더 낫다에는 마지막 이점이 있다. New Jersey 언어와 시스템은 복잡한 단일체 소프트웨어를 만들 만큼 वास्तव로 강력하지 않기 때문에, 대형 시스템은 구성 요소를 재사용하도록 설계되어야 한다. 따라서 통합의 전통이 생겨난다.
올바른 것은 어떨까? 두 가지 기본 시나리오가 있다. 크고 복잡한 시스템 시나리오와 다이아몬드 같은 보석 시나리오다.
크고 복잡한 시스템 시나리오는 다음과 같다.
먼저 올바른 것을 설계해야 한다. 그다음 구현을 설계해야 한다. 마지막으로 구현한다. 그것은 올바른 것이므로 원하는 기능의 거의 100%를 갖고, 구현 단순성은 결코 관심사가 아니었으므로 구현하는 데 긴 시간이 걸린다. 크고 복잡하다. 제대로 사용하려면 복잡한 도구가 필요하다. 마지막 20%에 노력의 80%가 들기 때문에, 올바른 것은 세상에 나오기까지 오래 걸리고 가장 정교한 하드웨어에서만 만족스럽게 실행된다.
다이아몬드 같은 보석 시나리오는 다음과 같다.
올바른 것은 설계하는 데 영원히 걸리지만, 그 과정의 모든 지점에서 꽤 작다. 빠르게 실행되도록 구현하는 것은 불가능하거나 대부분의 구현자의 능력을 벗어난다.
두 시나리오는 Common Lisp와 Scheme에 각각 대응한다.
첫 번째 시나리오는 고전적 인공지능 소프트웨어의 시나리오이기도 하다.
올바른 것은 흔히 단일체 소프트웨어이지만, 올바른 것이 흔히 단일체 방식으로 설계된다는 것 외에는 이유가 없다. 즉 이 특성은 우연한 결과다.
여기서 배울 교훈은 흔히 처음부터 올바른 것을 노리는 것이 바람직하지 않다는 점이다. 올바른 것의 절반을 사용할 수 있게 해서 바이러스처럼 퍼지게 하는 편이 낫다. 사람들이 그것에 빠져들면, 시간을 들여 올바른 것의 90%까지 개선하라.
잘못된 교훈은 이 비유를 글자 그대로 받아들이고 C가 AI 소프트웨어의 올바른 수단이라고 결론 내리는 것이다. 50% 해법은 기본적으로 옳아야 하는데, 이 경우에는 그렇지 않다.
그러나 Lisp 공동체가 Lisp 설계에 대한 자신의 입장을 진지하게 재고해야 한다는 결론은 내릴 수 있다. 이에 대해서는 나중에 더 말하겠다.
많은 Lisp 애호가는 Lisp 프로그래밍이 쉽다고 믿는다. 이는 어느 정도까지는 사실이다. 실제 애플리케이션을 배포해야 할 때는 코드 성능이 좋아야 한다. C에서는 컴파일러가 너무 많은 기술을 요구하고 데이터 타입이 너무 적어서 프로그래밍이 언제나 어렵다. Lisp에서는 성능이 아주 나쁜 프로그램을 작성하기가 매우 쉽다. C에서는 그렇게 하기란 거의 불가능하다. 다음의 성능이 나쁜 Lisp 프로그램 사례들은 배포를 의도한 실제 애플리케이션을 작성하면서 유능한 Lisp 프로그래머들이 모두 작성한 것이다. 나는 이것들이 꽤 슬프다.
이 예는 저지르기 쉬운 실수다. 여기서 프로그래머는 자신의 배열을 가능한 한 완전하게 선언하지 않았다. 따라서 각 배열 접근은 몇 개의 명령어여야 했는데 함수 호출만큼 느렸다. 원래 선언은 다음과 같았다:
(proclaim '(type (array fixnum *) *ar1* *ar2* *ar3*))
세 배열은 우연히도 고정 크기이며, 이는 다음의 올바른 선언에 반영되어 있다:
(proclaim '(type (simple-array fixnum (4)) *ar1*))
(proclaim '(type (simple-array fixnum (4 4)) *ar2*))
(proclaim '(type (simple-array fixnum (4 4 4)) *ar3*))
잘못된 선언을 바꾸자 전체 시스템 성능이 20% 향상되었다.
다음 예는 구현이 일반 설비의 특정 사례를 최적화하지 않았는데, 프로그래머가 그것이 빠를 것이라 생각하고 일반 설비를 사용한 경우다. 여기서는 부작용의 순서가 중요한 상황에서 다섯 값을 반환하고 있다:
(multiple-value-prog1
(values (f1 x)
(f2 y)
(f3 y)
(f4 y)
(f5 y))
(setf (aref ar1 i1) (f6 y))
(f7 x y))
구현은 최대 세 반환값까지는 multiple-value-prog1을 최적화하지만, 다섯 값의 경우에는 CONSes한다. 올바른 코드는 다음과 같다:
(let ((x1 (f1 x))
(x2 (f2 y))
(x3 (f3 y))
(x4 (f4 y))
(x5 (f5 y)))
(setf (aref ar1 i1) (f6 y))
(f7 x y)
(values x1 x2 x3 x4 x5))
프로그래머가 이 재작성 필요성을 알아야 할 이유는 없다. 반면 성능이 예상과 다르다는 사실 때문에 해당 프로그래머의 관리자가 실제로 내렸던 결론, 즉 Lisp가 잘못된 언어라는 결론으로 이어져서도 안 된다.
일부 Common Lisp 컴파일러는 다른 컴파일러와 같은 방식으로 최적화하지 않는다. 다음 표현식이 때때로 사용된다:
(* -1 <form>)
그러나 컴파일러는 흔히 이 변형에 대해 더 좋은 코드를 생성한다:
(- <form>)
물론 첫 번째는 다음 FORTRAN 관용구의 Lisp 유사물이다:
- -1*<form>
일부는 이 예를 믿기 어려울지도 모른다. 이것은 내가 본 어떤 코드에서 실제로 일어났다:
(defun make-matrix (n m)
(let ((matrix ()))
(dotimes (i n matrix)
(push (make-list m) matrix))))
(defun add-matrix (m1 m2)
(let ((l1 (length m1))
(l2 (length m2)))
(let ((matrix (make-matrix l1 l2)))
(dotimes (i l1 matrix)
(dotimes (j l2)
(setf (nth i (nth j matrix))
(+ (nth i (nth j m1))
(nth i (nth j m2)))))))))
더 나쁜 점은 특정 애플리케이션에서 행렬들이 모두 고정 크기였고, 행렬 산술이 Lisp에서도 FORTRAN만큼 빨랐을 것이라는 점이다.
이 예는 쓰라릴 만큼 슬프다. 코드는 절대적으로 아름답지만 행렬을 느리게 더한다. 따라서 훌륭한 프로토타입 코드이자 형편없는 제품 코드다. 아시다시피 C에서는 이만큼 나쁜 제품 코드를 작성할 수 없다.
더 나쁜 것이 더 낫다 세계에서 통합이란 .o 파일을 함께 링크하고, 함수를 자유롭게 상호 호출하며, 같은 기본 데이터 표현을 사용하는 것이다. 외부 적재기가 없고, 함수 호출 경계를 넘어 타입을 강제 변환하지 않으며, 한 언어를 지배적으로 만들지 않고, 구현 기술의 고충이 전체 시스템에 영향을 주게 하지 않는다.
위 현실과 마주하면 최고의 Lisp 외부 기능조차 그저 농담이다. 목록의 모든 항목은 Lisp 구현에서 해결할 수 있다. 다만 이것은 올바른 것 세계에서 Lisp 구현이 이루어진 방식이 아니다.
복잡한 유기체가 사산되는 동안 바이러스는 살아간다. Lisp가 적응해야지, 반대가 아니다. 올바른 것과 2 shillings가 있으면 차 한 잔을 살 수 있다.
이는 직시하기 어렵다. 예를 들어 대부분의 C 환경은 처음에는 Lisp 환경을 모방했지만 이제 꽤 좋아졌다. 현재 최고의 C 환경에는 다음이 있다:
기호 디버거 데이터 검사기 소스 코드 수준 한 단계 실행 내장 연산자 도움말 창 기반 디버깅 기호 스택 역추적 구조 편집기
그리고 곧 점진적 컴파일과 적재도 갖게 될 것이다. 이 환경들은 다른 언어로 쉽게 확장될 수 있으며, 다언어 환경도 머지않았다.
여전히 최고이지만, 현재 Lisp 환경에는 몇 가지 두드러진 실패가 있다. 첫째, 창 기반인 경향은 있지만 잘 통합되어 있지 않다. 즉 관련 정보가 관계를 전달하도록 표현되지 않는다. 수많은 창은 통합을 뜻하지 않으며, 같은 언어로 구현되고 같은 이미지에서 실행되는 것도 마찬가지다. 사실 현재 이용 가능한 어떤 Lisp 환경도 심각한 수준의 통합을 갖고 있지 않다고 나는 믿는다.
둘째, 지속적이지 않다. 단일 로그인 세션을 위해 정의된 듯하다. 영속 데이터를 유지하는 데 파일을 사용한다. 얼마나 1960년대적인가.
셋째, 외부 인터페이스를 이용할 수 있을 때조차 다언어적이지 않다.
넷째, 소프트웨어 생명주기를 광범위한 방식으로 다루지 않는다. 문서화, 명세, 유지보수, 테스트, 검증, 수정, 고객 지원은 모두 무시된다.
다섯째, 적절한 시점에 정보를 활용하지 않는다. 컴파일러는 일부 정보를 제공할 수 있지만, 환경은 무엇이 완전히 정의되었고 무엇이 부분적으로 정의되었는지를 일반적으로 알 수 있어야 한다. 성능 모니터링이 고역이어서는 안 된다.
여섯째, 환경 사용이 어렵다. 알아야 할 것이 너무 많다. 작동 방식을 관리하기가 너무 어렵다.
일곱째, 거의 모든 흥미로운 소프트웨어가 이제 집단으로 작성되는데도 환경은 다중 사용자를 지원하지 않는다.
진짜 문제는 지난 10년 동안 Lisp 환경에서 거의 진전이 없었다는 점이다.
해가 뜨면, 나는 꼭대기에 있을 거야. 너는 바로 아래에서 올려다보겠지. 여기 올라오는 길에, 나는 네가 այնտեղ 기다리는 걸 보게 될 거야. 나는 네 곁으로 가는 중이야. 이제 나는 거기에 닿을 거라는 걸 알아.
? & The Mysterians
이 우울한 막간은 행복한 결말을 맞을 수 있다.
ISO 수준에서 우리의 차이를 묻고 단기 필요와 장기 필요가 있음을 깨달아야 한다. 단기 필요는 Common Lisp여야 하며, 장기 필요는 실용적 애플리케이션을 위한 모든 문제를 다뤄야 한다.
올바른 것 태도가 너무 많은 문제를 해결하는 매우 크고, 이해하기 복잡하며, 구현하기 복잡한 Lisp인 Common Lisp를 가져왔음을 보았다. 미래를 위해 Common Lisp 너머로 나아가야 하지만, 그렇다고 지금 Common Lisp를 포기한다는 뜻은 아니다. 애플리케이션 배포가 가능함을 보았고, 배포용 애플리케이션을 더 쉽게 작성하도록 하는 도구를 제공할 수 있다고 생각한다. 여러 면에서 Common Lisp를 올바른 것의 지점까지 이르게 하는 데 많은 작업이 들어갔고, 실행 가능한 상업 구현도 있다. 그러나 배포와 통합 문제는 대대적으로 해결해야 한다.
앞서 MIT 접근법은 흔히 사산 결과를 낳는다고 특징지었다. 지금 Common Lisp 표준화를 멈추는 것은 낙태와 같고, 이는 Lisp 공동체가 Lisp를 포기하는 것과 같다. New Jersey 접근법을 채택하고 싶다면 Lisp를 포기하는 것은 잘못이다. C는 AI에 알맞은 언어가 아니기 때문이다.
또한 지금 Common Lisp를 버리고 새 표준을 작업한 뒤 적시에 표준화하는 것은 단순히 불가능하다. 현재 우리에게 있는 것은 Common Lisp뿐이다. 다른 어떤 방언도 표준화 준비가 되지 않았다.
Scheme은 더 작은 Lisp지만, MIT 접근법의 문제도 겪는다. 너무 빡빡하고 대규모 소프트웨어에 적합하지 않다. 적어도 Common Lisp에는 이를 위한 몇몇 설비가 있다.
Common Lisp에는 국제적으로 인정된 표준이 있어야 한다고 생각한다. 상업 문제에 가장 좋은 해법이 아니라는 이유만으로 오늘 Common Lisp 노력을 낙태시켜서 얻을 것이 무엇인지 알 수 없다. Lisp가 죽었거나 죽어간다고 믿는 이들에게 Common Lisp를 없애는 일은 Lisp 공동체가 자기 종족을 죽인다고 사람들에게 확신시키는 것 외에 무엇을 이루는가? 여러 Lisp 표준을 가져도 해롭지 않은데 Common Lisp가 표준이 되는 것을 막는 데 드는 노력이 줄었으면 한다.
반면 다음 세대 Lisp를 향한 강력한 노력은 있어야 한다. 공동체로서 제자리에 멈춰서는 안 되며, 최악의 일은 바로 그것인데 현재 그러고 있다.
모든 이해관계자는 장기 노력을 위해 나서야 한다.
단일체 환경을 만드는 환경 경로를 따르는 것은 실수라고 생각한다. 환경에서 다양한 도구를 사용할 수 있어야 하며, 새 도구를 만드는 이들이 그것을 환경에 통합할 수도 있어야 한다.
모든 언어 처리기를 포함한 모든 도구가 프로토콜 기반인 개방형 아키텍처 위에 구축되면서도 긴밀히 통합된 환경을 만들 수 있다고 믿는다. 사용자에게 특정 소프트웨어 방법론을 강요하지 않으면서 다언어적이고 소프트웨어 생명주기 문제를 다루는 환경을 만들 수 있다고 믿는다.
우리 환경은 기존 환경이 하는 방식으로 비-Lisp 프로그래머를 차별해서는 안 된다. Lisp는 세계의 중심이 아니다.
Common Lisp가 커널과 라이브러리 구조로 되어 있지는 않지만, 그런 방식으로 구현할 수 있다. 커널과 라이브러리 루틴은 다른, 어쩌면 비-Lisp 모듈과 쉽게 링크할 수 있도록 .o 파일 형태일 수 있다. 구현은 예를 들어 작은 유틸리티 프로그램을 작성할 수 있게 해야 한다. 특히 공통 백엔드를 사용하는 기존 컴파일러를 활용할 수도 있다. 확장을 포함할 수도 있는 표준 디버거가 Lisp 코드에서 작동하도록 Lisp를 구현할 수도 있다.
표준 도구 개발자들이 Lisp를 위해 도구를 확장하는 데 동의하기까지 시간이 걸릴 수 있지만, 우리의 뛰어난 언어가 일반 언어처럼 구현되기 전까지는 결코 일어나지 않을 것이다.
다른 어떤 언어에도 유불리한 차별이 없는 Lisp와 주변 환경을 구현할 수 있다고 믿는다. 다언어 환경, 영리한 Lisp 데이터 표현, 보수적 가비지 컬렉션, 관습적 호출 프로토콜을 사용하면 단점이 전혀 없는 완전히 통합된 Lisp를 만들 수 있다.
Lisp는 여전히 최고의 프로토타이핑 언어다. 이를 밀어붙여야 한다. 다언어 환경은 다언어 프로토타이핑 시스템의 기반 또는 인프라를 형성할 수 있다. 이는 Lisp의 강점을 활용하고 새 강점을 도입할 새로운 방법을 찾기 위해 더 많은 연구를 한다는 뜻이다.
프로토타이핑은 복잡한 시스템의 초기 구현을 만드는 행위다. 프로토타입은 쉽게 계측하고, 모니터링하고, 변경할 수 있다. 프로토타입은 흔히 새 목적에 맞춰 조정된 이질적인 부분들로 구축된다. 프로토타입의 구축 설명에는 기존 프로그램의 행동 특성을 수정한다는 진술이 흔히 포함된다. 예를 들어 트리 순회 프로그램이 있다고 하자. 이 프로그램을 사용하는 프로토타입 설명은 다음과 같은 말로 시작할 수 있다.
let S1 be the sequence of leaf nodes visited by P on tree T1 and S2 the
leaf nodes visited by P on tree T2. Let C be a correspondence between
S1 and S2 where f: S1 ! S2 maps elements to corresponding elements.
후속 진술은 대응 관계를 조작하고 f를 사용할 수 있다. 리프 노드의 정의가 명시되면, 이는 시스템이 대응 관계와 f를 지원하도록 순회 루틴을 수정할 수 있을 만큼 충분히 정확한 진술이다.
기존 프로그램의 수정과 제어를 기술하는 언어는 프로그램 언어라고 부를 수 있다. 프로그램 언어는 하나 또는 여러 기저 프로그래밍 언어 위에 구축될 수 있고, 실제로 프로토타이핑 환경 기능의 일부로 구현될 수 있다. 이 관점은 환경이 소스 텍스트 준비를 포함하여 프로그래머가 작동하는 프로그램을 만들도록 돕는 메커니즘이라는 통찰 위에 세워진다. 환경이 가공되지 않은 소스 텍스트로만 작업해야 한다는 필수 요건은 없다. 다른 예로, 일부 시스템은 채널을 통해 통신하는 여러 프로세스로 구성된다. 시스템의 이 부분 생성은 시각적일 수 있으며, 환경이 만드는 최종 결과는 여러 언어의 소스 코드 집합, 빌드 스크립트, 링크 지시문, 운영 체제 호출이 된다. 어느 하나의 프로그래밍 언어도 프로그램 언어를 포괄하지 않으므로, 그러한 언어를 에피 언어라고 부를 수 있다.
나는 다음 Lisp가 있을 것이라고 생각한다. 이 Lisp는 더 나쁜 것이 더 낫다에서 본 성공 원칙을 사용하여 신중하게 설계되어야 한다.
Lisp에는 단순하고 쉽게 구현할 수 있는 커널이 있어야 한다. 그 커널은 Scheme보다 많아야 한다. 모듈과 매크로가 있어야 한다. 또한 Scheme보다 적어야 한다. continuations는 그 밖에는 깔끔한 Scheme 원고에 남은 보기 흉한 얼룩이다.
커널은 구현 단순성을 강조해야 하지만, 인터페이스 단순성을 희생해서는 안 된다. 둘이 충돌하는 곳에서는 그 기능을 커널에서 빼야 한다. 한 가지 이유는 GNU Emacs가 Emacs 매크로 정의에 Lisp 버전을 사용하는 것처럼, 커널이 다른 시스템의 확장 언어가 될 수 있도록 하기 위해서다.
Common Lisp의 극단적 동적성 일부는 재검토하거나 적어도 절충을 다시 고려해야 한다. 예를 들어 실제 프로그램이 얼마나 자주 다음을 하는가?
(defun f ...)
(dotimes (...)
...
(setf (symbol-function 'f) #'(lambda ...))
...)
다음 Lisp의 구현은 다른 모든 함수 호출의 나쁜 성능을 대가로 특히 이 작업을 빠르게 만들려는 이전 구현의 영향을 받아서는 안 된다.
언어는 적어도 네 계층으로 나뉘어야 한다:
작고 구현하기 단순한 커널 언어. 모든 경우에 동적 재정의 필요성을 재검토하여 이 수준에서 지원이 필요한지 판단해야 한다. 커널의 어떤 것도 동적으로 재정의 가능할 필요는 없다고 나는 믿는다. 언어를 살찌우는 언어적 계층. 이 계층에는 구현상 어려움이 있을 수 있으며, 커널에는 너무 비싸지만 빼기에는 너무 중요한 동적 측면을 아마 갖게 될 것이다. 라이브러리. Common Lisp에 있는 대부분은 이 계층에 있게 된다. 환경이 제공하는 에피언어적 기능.
첫 번째 계층에는 조건문, 함수 호출, 모든 원시 데이터 구조, 매크로, 단일 값, 아주 기본적인 객체 지향 지원을 넣는다.
두 번째 계층에는 다중 값과 더 정교한 객체 지향 지원을 넣는다. 두 번째 계층은 환경이 제공하도록 맡기기에는 너무 중요하지만 정확한 정의를 보장할 만큼 충분한 의미론적 결과를 가진 어려운 프로그래밍 구조를 위한 것이다. 어떤 형태의 재정의 기능도 여기에 있을 수 있다.
세 번째 계층에는 시퀀스 함수, 정교한 IO 함수, 첫 번째 계층과 어쩌면 두 번째 계층에서 단순히 구현되는 그 밖의 모든 것을 넣는다. 이 함수들은 링크 가능해야 한다.
네 번째 계층에는 환경이 제공할 수 있고 제공해야 하지만 표준화되어야 하는 기능을 넣는다. 전형적인 예는 CLOS의 defmethod다. CLOS에서 제네릭 함수는 메서드들로 만들어지며, 각 메서드는 특정 클래스에 적용 가능하다. 첫 번째 계층에는 완전한 제네릭 함수, 즉 한 곳에 정의된 모든 메서드와 함께 제네릭 함수를 위한 정의 형식이 있다(이는 계층 1 컴파일러가 보고 싶어 하는 방식이다). 제네릭 함수에 이름을 연결하는 수단도 있을 것이다. 하지만 시스템을 개발하는 동안 클래스는 여러 곳에서 정의될 것이고, 이 클래스들에 인접해서 관련된(적용 가능한) 메서드를 볼 수 있으면 합리적이다. defmethod는 메서드를 정의하는 구조이며, defmethod 형식은 다른 정의 형식들 사이 어디에나 놓일 수 있다.
그러나 메서드는 그 메서드가 특수화된 각 클래스뿐 아니라 그 클래스들의 각 하위 클래스와도 관련이 있다. 그렇다면 고유한 defmethod 형식은 어디에 놓아야 하는가? 환경은 실제 정의가 특정 장소에 있는 동안 프로그래머가 이 모든 장소 중 어느 곳에서나 또는 모두에서 메서드 정의를 볼 수 있게 해야 한다. 그 장소는 단일 제네릭 함수 정의 형식 안인 편이 좋으며, 필요할 때 관련 클래스 근처에 defmethod 동등물을 보여 주고 defmethod 형식의 소스를 입력으로 받아들이는 것은 환경의 몫이다(환경은 그것을 제네릭 함수 정의 안에 배치한다).
우리는 defmethod 형식을 표준화하고 싶지만, 그것은 환경이 제공하는 언어적 기능이다. 마찬가지로 키워드 인수처럼 정교한 lambda-list 문법의 많은 사용은, 어쩌면 색상이나 텍스트에 대한 다른 보조 수단을 사용해 환경이 제공할 수 있는 언어적 지원 사례다.
사실 함수-함수 인터페이스 영역은 어떤 종류의 인수 명명 체계가 필요한지, 그리고 그것들이 어느 계층에 놓여야 하는지를 보기 위해 재검토되어야 한다.
마지막으로, 모든 계층 2 기능은 환경에 의해 계층 1 구현에서 제공될 수 있을지도 모른다는 점에 유의하라.
Lisp 공동체에는 애플리케이션 작성자가 너무 적다. Lisp 공급업체는 이 애플리케이션 작성자들이 반드시 이기게 해야 한다. 그러려면 관련 당사자들이 자기 문제를 공개하고 적대적이지 않아야 한다. 예를 들어 전문가 시스템 셸 회사가 문제를 발견하면 Lisp 공급업체에 소스 코드를 공개하여, 둘 모두 더 빠르고 작으며 배포하기 좋은 제품이라는 공통 목표를 향해 일할 수 있어야 한다. Lisp 공급업체도 똑같이 해야 한다.
AI 공동체의 사업 리더십은 비밀주의, 불신, 점수 차이를 벌리는 경쟁성이라는 사업 관행의 최악의 희화화된 특성을 채택한 듯하다. 우리는 우리 자신의 대열에서 경쟁자를 찾지 않아도 충분한 공통 경쟁자가 있는 산업이다.
때로는 해도 다시 뜬다.
참고문헌 [1] ? & the Mysterians, 96 Tears, Pa-go-go Records 1966, Cameo Records에서 1966년 9월 재발매.
Erik Naggum이 HTML화함