검수요청.png검수요청.png

리스프

위키원
이동: 둘러보기, 검색

리스프(Lisp, LISP)는 프로그래밍 언어의 한 종류로 대표적인 함수형 언어이다.

프로그래밍 언어의 역사를 말할 때, 현대의 컴퓨터를 위해 등장한 고급 언어 중 가장 오래된 것이 포트란이고, 두 번째로 오래된 것이 바로 이 리스프다. 역사가 긴 만큼 소위 Lisp-family라 불리는 여러 가지 사투리가 존재하고 이것들이 너무 다양하게 난립했기 때문에 공통 표준 규격을 만들어 1984년에 커먼 리스프(Common Lisp)가 탄생했다. 보통 리스프라고 말하면 이러한 사투리까지 모두 포함하는 의미로 사용된다. 최초 표기는 LISP이었지만 어느새 Lisp으로 표기되기 시작했으며, 지금은 Lisp가 널리 쓰인다.

개요[편집]

리스프 혹은 리습프로그래밍 언어의 계열로서, 오랜 역사와 독특하게 괄호를 사용하는 문법으로 유명하다. 1958년에 초안이 작성된 이 언어는 현재 널리 사용되는 포트란에 이어 두 번째로 오래된 고급 프로그래밍 언어이다. 리스프는 포트란처럼 초반에 많은 변화를 겪어야 했으며 수많은 변종들이 존재한다. 오늘날 가장 널리 알려진 일반 리스프 변종은 커먼 리스프와 스킴이다. 그리고 리스프의 본래 뜻은 혀가 꼬이기 쉬운 발음이라는 뜻이다.

리스프는 본래 실용적인 목적 아래 컴퓨터 프로그램을 활용하여 수학 표기법을 나타내기 위한 목적으로 만들어졌다. 이는 알론소 처치의 람다 대수의 표기법에 영향을 많이 받았다. 곧이어 이는 인공지능 연구소에서 가장 인기있는 언어가 되었다. 초기 프로그래밍 언어 중의 하나인 리스프는 컴퓨터 과학의 많은 개념들의 선구자로서 트리 자료구조, 쓰레기 수집, 동적 자료형과 인터프리터와 같은 개념들을 개척했다.

LISP 라는 이름 자체는 "LISt Processing"(리스트 프로세싱)의 줄임말이다. 연결 리스트는 리스프의 주요 자료구조 중 하나로서, 리스프 코드는 그 자체로 하나의 리스트이다. 그 결과로, 리스프 프로그램은 소스 코드를 자료 구조를 다듬는 수준으로 재배치할 수 있게 된다. 리스프 매크로 시스템을 통해 프로그래머는 새로운 소스 코드를 만들 수 있으며, 심지어 기존 리스프에 내장되는 새로운 언어, 그것도 특정 목적을 위한 용도의 언어를 만들어낼 수 있다.

코드와 데이터의 교환 가능성은 Lisp의 즉각적으로 인식할 수 있는 문법을 제공한다. 모든 프로그램 코드는 s-표현식, 즉 괄호로 묶인 리스트로 작성된다. 함수 호출이나 문법 형태는 함수나 연산자의 이름을 먼저 쓰고, 그 뒤에 인수를 나열하는 방식으로 작성된다. 예를 들어, 세 개의 인수를 받는 함수 f를 호출하는 형식은 (f arg1 arg2 arg3)와 같다.

역사[편집]

존 매카시는 1958년 매사추세츠 공과대학교(MIT)에서 Lisp를 개발하기 시작했다. 그는 IBM 704 컴퓨터에서 작동할 수 있는 AI 프로그래밍 언어를 만들고자 했으며, "IBM은 인공지능 연구를 활발히 진행할 좋은 선택지처럼 보였다"고 믿었다. 매카시는 정보 처리 언어에 영감을 받았지만, 이는 다른 하드웨어용으로 설계되어 있었고, 더 수학적인 언어를 선호했기에 사용하지 않았다. 이러한 이유들로 인해 그는 포트란 목록 처리 언어(Fortran List Processing Language) 설계에 참여했으며, 이는 포트란(Fortran) 라이브러리로 구현되었다. 그러나 이 언어는 재귀나 현대적인 if-then-else 문을 지원하지 않았기 때문에 그에게 만족을 주지 못했다(이는 Lisp가 처음 도입될 때 새로워 보였던 개념이었다).

매카시의 초기 표기법은 M-표현식(괄호로 묶인 표현식)을 사용했으며, 이는 S-표현식으로 변환되었다. 예를 들어, M-표현식 car[cons[A,B]]는 S-표현식 (car (cons A B))와 같다. Lisp가 구현되자 프로그래머들은 빠르게 S-표현식을 사용하기로 선택했으며, M-표현식은 버려졌다. M-표현식은 Horace Enea의 MLisp와 Vaughan Pratt의 CGOL에서 잠깐 등장했다.

Lisp의 첫 번째 구현

Lisp는 Steve Russell이 IBM 704 컴퓨터에서 펀치 카드로 구현한 최초의 프로그래밍 언어였다. 당시 스티브 러셀(Steve Russell)은 매카시와 함께 일하고 있었으며, Lisp의 eval 함수가 기계어로 구현될 수 있다는 것을 깨달았다. 매카시는 처음에 이 eval 함수가 이론적이기 때문에 실용적으로 구현할 수 없다고 생각했지만, 러셀은 이를 실현시켰고, 결국 이를 Lisp 인터프리터로 광고했다. 이로써 Lisp는 오늘날 우리가 아는 형태로 기본적으로 완성되었다.

매카시는 다음과 같이 회상한다:

"스티브 러셀이 말하길, '왜 이 eval을 내가 프로그래밍하지 않겠습니까?' 그리고 나는 그에게, '호, 호, 너는 이론과 실제를 혼동하고 있다, 이 eval은 읽기 위한 것이지 계산을 위한 것이 아니다'라고 말했지만, 그는 이를 진행했고 결국 그것을 IBM 704 기계어로 컴파일했고, 오류를 수정한 후 이를 Lisp 인터프리터로 광고했다."

그 결과, Lisp 프로그램을 실행하거나 더 정확히 말하자면 "Lisp 표현식을 평가"하는 데 사용할 수 있는 작업하는 Lisp 인터프리터가 만들어졌다.

기본 연산
car와 cdr

IBM 704의 두 가지 어셈블리 언어 매크로는 리스트를 분해하는 기본 연산이 되었다: car (주소 부분의 내용)와 cdr (감소 부분의 내용)이다. 여기서 "레지스터"는 컴퓨터의 중앙 처리 장치(CPU) 레지스터를 의미한다. Lisp 방언은 여전히 car와 cdr을 사용하여 리스트에서 첫 번째 항목과 나머지 항목을 각각 반환하는 연산을 수행한다.

Lisp 설계의 발표

매카시는 1960년 4월 1일, ACM 커뮤니케이션에 발표된 논문 "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I"에서 Lisp의 설계를 발표했다. 이 논문에서 그는 몇 가지 간단한 연산자와 Church로부터 빌려온 익명 함수 표기법을 사용하여 알고리즘을 위한 튜링 완전 언어를 만들 수 있음을 보여주었다.

첫 번째 완전한 Lisp 컴파일러

Tim Hart와 Mike Levin은 1962년에 MIT에서 Lisp로 작성된 첫 번째 완전한 Lisp 컴파일러를 구현했다. 이 컴파일러는 기존의 Lisp 인터프리터로 컴파일러 코드를 해석하고, 이 코드가 실행될 수 있는 기계어 출력을 생성하여 인터프리터보다 40배 빠른 속도로 실행되었다. 이 컴파일러는 증분 컴파일이라는 Lisp 모델을 도입했으며, 이는 컴파일된 함수와 해석된 함수가 자유롭게 섞여서 사용할 수 있도록 했다. Hart와 Levin의 메모에서 사용된 언어는 McCarthy의 초기 코드보다 현대적인 Lisp 스타일에 훨씬 가까웠다.

가비지 컬렉션(Garbage collection)

MIT 졸업생인 다니엘 에드워드(Daniel Edwards)는 1962년 이전에 가비지 컬렉션 루틴을 개발했다.

1980년대와 1990년대의 Lisp 통합 노력

1980년대와 1990년대에는 Maclisp의 후속 언어인 ZetaLisp와 NIL(New Implementation of Lisp) 등을 포함한 새로운 Lisp 방언들의 작업을 통합하려는 노력이 있었다. 이 새로운 언어인 Common Lisp는 그 이전에 사용되던 방언들과 어느 정도 호환되었다(책 Common Lisp the Language는 다양한 구성 요소들의 호환성을 언급하고 있다). 1994년, ANSI는 Common Lisp 표준을 발표했으며, 이는 "ANSI X3.226-1994 Information Technology Programming Language Common Lisp"로 알려져 있다.

타임라인[편집]

리스프 방언의 타임라인.png

인공지능과의 연결[편집]

Lisp는 처음부터 인공지능(AI) 연구 커뮤니티와 밀접하게 연결되어 있었으며, 특히 PDP-10 시스템에서 사용되었다. Lisp는 유명한 AI 시스템인 SHRDLU에서 사용된 언어인 Micro Planner의 구현 언어로 사용되었다. 1970년대에는 AI 연구가 상업적 후속 사업을 생성하면서, 기존의 Lisp 시스템의 성능이 중요한 문제가 되었고, 프로그래머들은 Lisp 구현에 관련된 다양한 기술과 선택들이 성능에 미치는 영향을 잘 이해해야 했다.

변종[편집]

1970년대에 가장 많이 쓰던 리스프 변종은, MIT의 프로젝트 맥(MAC)에서 만들었던 맥리스프와, Bolt Beranek & Newman 사와 제록스 팰러앨토 연구소가 함께 만든 인터리스프(Interlisp)이다. 포터블 스텐다드 리스프(Portable Standard Lisp)는 여러 기기로 쉽게 옮겨 심을 수 있도록 설계한, 리스프의 변종이다. 또한 맥리스프(MacLisp)를 기반으로 한 여러 변종이 나왔는데, UC 버클리에서 만든 프렌즈 리스프(Franz Lisp)가 있고, MIT의 인공지능 실험실에서 만든 제타리스프(Zetalisp)가 있다. 제타리스프는 전용 프로세서를 통해 리스프를 매우 효율적으로 돌리려고 만든 것이다.

리스프 변종 가운데 가장 널리 쓰고 있는 것으로는 커먼 리스프(Common Lisp)와 스킴(Scheme)을 들 수 있다.

커먼 리스프[편집]

커먼 리스프는 그 때까지 여러 변종에 있던 기능을 한데 묶어서 리스프 산업 표준을 정하려고 리스프 공동체가 만든 것이다. 1994년에 커먼 리스프(Common Lisp)의 ANSI 표준이 정해졌다.

스킴[편집]

스킴은 MIT 인공지능 실험실의 Guy Lewis Steele Jr., Gerald Jay Sussman이 1975년에 처음 내놓은 것을 나중에 MIT에서 컴퓨터과학과 학생들을 가르치기 위해 다듬은 것이다. 여기에 사용된 교재가 흔히 SICP 또는 위자드 북(Wizard book)이라고도 하는 《Structure and Interpretation of Computer Programs》이다.[2] 1990년에 스킴의 IEEE 표준이 정해졌다.

역사적으로 중요한 Lisp 방언들[편집]

  • LISP 1: 첫 번째 구현.
  • LISP 1.5: MIT에서 John McCarthy와 다른 사람들에 의해 개발된 최초의 널리 배포된 버전. "LISP 1" 인터프리터에서 여러 가지 개선이 이루어졌지만, 계획된 LISP 2와 같은 주요 구조적 재설계는 아니었다.
  • Stanford LISP 1.6: Stanford AI Lab에서 개발된 LISP 1.5의 후속 버전으로, PDP-10 시스템과 TOPS-10 운영 체제에서 널리 배포되었다. 나중에 Maclisp와 InterLisp로 대체되었다.
  • Maclisp: MIT의 Project MAC을 위해 개발된 MACLISP는 LISP 1.5의 직접적인 후손이며, PDP-10과 Multics 시스템에서 실행되었다. MACLISP는 나중에 Maclisp라고 불리게 되었으며, 종종 MacLisp로 불린다. "MAC"은 Apple의 Macintosh나 McCarthy와는 관련이 없다.
  • Interlisp: BBN Technologies에서 PDP-10 시스템과 TENEX 운영 체제를 위해 개발되었으며, 후에 Xerox Lisp 머신용 "West coast" Lisp로 InterLisp-D가 채택되었다. "InterLISP 65"라는 작은 버전은 MOS Technology 6502 기반의 Atari 8비트 컴퓨터용으로 출시되었다. Maclisp와 InterLisp는 강력한 경쟁 관계에 있었다.
  • Franz Lisp: 원래 캘리포니아 대학교 버클리 캠퍼스에서 시작된 프로젝트로, 후에 Franz Inc.에 의해 개발되었다. 이름은 "Franz Liszt"에서 유래된 농담으로, 후에 Franz Inc.에서 판매한 Common Lisp의 방언인 Allegro Common Lisp와는 관련이 없다.
  • XLISP: AutoLISP의 기초가 된 방언.
  • Standard Lisp와 Portable Standard Lisp: 특히 Computer Algebra System인 REDUCE와 함께 널리 사용되었으며, 다양한 플랫폼에서 이식되었다.
  • ZetaLisp: Lisp 머신에서 사용되었으며, Maclisp의 직접적인 후손이다. ZetaLisp는 Common Lisp에 큰 영향을 미쳤다.
  • LeLisp: 프랑스의 Lisp 방언으로, SOS Interface라는 최초의 인터페이스 빌더 중 하나가 LeLisp로 작성되었다.
  • Scheme (1975): Lisp의 또 다른 중요한 방언으로, 많은 영향을 미쳤다.
  • Common Lisp (1984): 여러 개의 분기된 시도들(ZetaLisp, Spice Lisp, NIL, S-1 Lisp)을 통합한 것으로, Scheme 방언의 영향도 받았다. Common Lisp는 여러 플랫폼에서 널리 사용되었으며, 많은 사람들이 이를 사실상 표준으로 받아들였다. ANSI Common Lisp(ANSI X3.226-1994)가 발표되기 전까지 사실상의 표준이었다. Common Lisp의 주요 서브 방언으로는 Steel Bank Common Lisp (SBCL), CMU Common Lisp (CMU-CL), Clozure OpenMCL(Clojure와 혼동하지 말 것), GNU * * * CLisp, 후에 버전 업된 Franz Lisp 등이 있다. 이들은 모두 후에 발표된 ANSI CL 표준을 따르고 있다.
  • Dylan: 첫 번째 버전에서 Scheme과 Common Lisp Object System(CLOS)을 혼합한 언어.
  • EuLisp: 효율적이고 정리된 Lisp을 개발하려는 시도.
  • ISLISP: 효율적이고 정리된 Lisp을 개발하려는 시도. ISO/IEC 13816:1997로 표준화되었으며, 후에 ISO/IEC 13816:2007로 개정되었다.
  • IEEE Scheme: IEEE 표준 1178-1990 (R1995).
  • ANSI Common Lisp: 미국 국가 표준화 기구(ANSI)에서 발표한 Common Lisp 표준으로, X3J13 소위원회에 의해 만들어졌다. 이는 Common Lisp: The Language를 기반 문서로 사용하여 공공의 합의 과정을 거쳐 프로그램의 이식성 및 구현 간의 호환성 문제를 해결하려 했다. 공식적으로는 ANSI 표준이지만, 전 세계적으로 구현, 판매, 사용, 영향력이 계속되고 있다.
  • ACL2 (A Computational Logic for Applicative Common Lisp): 부수 효과가 없는 적용형(Common Lisp의 변형) Lisp로, 컴퓨터 시스템을 모델링하고 그 모델에 대해 성질을 증명하는 데 사용할 수 있는 언어이자 도구이다.
  • Clojure: Java 가상 머신에서 실행되며, 특히 동시성(concurrency)을 강조하는 최근의 Lisp 방언.
  • Game Oriented Assembly Lisp (GOAL): Naughty Dog에서 개발한 비디오 게임 프로그래밍 언어로, Allegro Common Lisp를 사용하여 작성되었으며, Naughty Dog에서 개발한 Jak and Daxter 시리즈의 게임 개발에 사용되었다.

2000년부터 현재까지의 Lisp[편집]

1990년대에는 다소 침체된 상황이었지만, 2000년 이후 Lisp에 대한 관심이 재차 증가했다. 대부분의 새로운 활동은 Common Lisp, Scheme, Emacs Lisp, Clojure, Racket 구현과 관련된 것으로, 새로운 포터블 라이브러리와 애플리케이션의 개발이 이루어졌다.

많은 새로운 Lisp 프로그래머들이 Paul Graham과 Eric S. Raymond와 같은 저자들에게 영향을 받아 다른 사람들이 구식이라고 여기는 언어를 배우게 되었다. 새로운 Lisp 프로그래머들은 종종 이 언어가 눈을 뜨게 해주는 경험이라고 묘사하며, 다른 언어보다 생산성이 훨씬 높다고 주장한다. 이러한 인식의 증가는 "AI 겨울"과 Lisp가 1990년대 중반에 잠깐 상승했던 것을 대비할 수 있다.

2010년경, Common Lisp 구현체가 11개 이상 활성 유지되고 있었다.

오픈 소스 커뮤니티의 기여
  • CLiki: Common Lisp 관련 정보를 모은 위키.
  • Common Lisp Directory: 리소스를 나열한 디렉토리.
  • #lisp IRC 채널: 코드 스니펫을 공유하고 논의하는 인기 IRC 채널.
  • Planet Lisp: 여러 Lisp 관련 블로그의 내용을 모은 사이트.
  • LispForum: Lisp 관련 주제를 논의하는 사용자 포럼.
  • Lispjobs: 취업 공고를 발표하는 서비스.
  • Weekly Lisp News: Lisp 관련 주간 뉴스 서비스.
  • Common-lisp.net: 오픈 소스 Common Lisp 프로젝트를 위한 호스팅 사이트.
  • Quicklisp: Common Lisp의 라이브러리 관리 시스템.

Lisp50@OOPSLA에서 Lisp의 50주년을 기념하는 행사가 열렸고, Boston, Vancouver, Hamburg에서 정기적인 사용자 모임이 열리고 있다. 또한, European Common Lisp Meeting, European Lisp Symposium, International Lisp Conference와 같은 다양한 행사들이 개최되고 있다.

Scheme 커뮤니티는 20개 이상의 구현체를 활발히 유지하고 있다. 2000년대에는 Chicken, Gambit, Gauche, Ikarus, Larceny, Ypsilon와 같은 중요한 새 구현체들이 개발되었다. Revised5 Report on the Algorithmic Language Scheme 표준은 Scheme 커뮤니티에서 널리 받아들여졌으며, **Scheme Requests for Implementation(SRFIs)**는 Scheme에 대한 많은 사실상의 표준 라이브러리와 확장을 만들어냈다. 일부 Scheme 구현체의 사용자 커뮤니티는 계속해서 성장하고 있다. 2003년에는 새로운 언어 표준화 과정이 시작되어, 2007년에 R6RS Scheme 표준이 발표되었다. Scheme을 컴퓨터 과학 교육에서 사용하는 대학은 줄어들었으며, MIT는 이제 Python을 학부 컴퓨터 과학 프로그램과 MITx 대규모 온라인 공개 강좌에서 사용하고 있다.

새로운 Lisp 방언들
  • Arc: Paul Graham이 개발한 Lisp 방언.
  • Hy: Python과 통합될 수 있는 Lisp 방언.
  • Nu: Common Lisp에서 영감을 받은 새로운 방언.
  • Liskell: Haskell의 영향을 받은 Lisp 방언.
  • LFE (Lisp Flavored Erlang): Erlang을 기반으로 한 Lisp 방언.

Julia의 파서는 Scheme의 방언인 Femtolisp로 구현되었으며, Julia는 Scheme에서 영향을 받아 개발되었다.

2019년 10월, Paul Graham은 "Bel"이라는 새로운 Lisp 방언의 사양을 발표했다.

다른 프로그래밍 언어에 미친 영향[편집]

언어 자체가 새로운 기능을 탑재하기 걸맞게 유연하기도 하고, 오랫동안 학계의 특정 분야에서 거의 표준적으로 사용된 언어였기 때문에 리스프가 후대 언어에 끼친 영향은 매우 크다. if-then-else 형식, 재귀함수, 쓰레기 수집, JIT, 동적 타이핑, 동적 메모리 할당, 일급 함수 객체, 액터 모델 등 명령형이고 함수형이고를 불문하고 간접적인 영향까지 포함하면 프로그래밍 언어 중 리스프의 영향을 받지 않은 것을 찾아보기 힘들 정도이다.

주요 방언[편집]

Lisp의 주요 방언은 Common Lisp와 Scheme으로, 이 두 언어는 디자인 선택에서 크게 다르다.

Common Lisp는 Maclisp의 후계후계 언어이다. 주요 영향은 Lisp Machine Lisp, Maclisp, NIL, S-1 Lisp, Spice Lisp, 그리고 Scheme이었다. 공통 Lisp은 Lisp Machine Lisp(대형 Lisp 다이얼렉트로 Lisp 머신을 프로그래밍하는 데 사용됨)의 많은 기능을 갖추고 있지만, 개인용 컴퓨터나 워크스테이션에서 효율적으로 구현될 수 있도록 설계되었다. 공통 Lisp은 범용 프로그래밍 언어로서 데이터 유형, 함수, 매크로, 기타 언어 요소들이 많이 포함된 큰 언어 표준을 가지고 있으며, 객체 시스템(공통 Lisp 객체 시스템)을 갖추고 있다. 공통 Lisp은 Scheme에서의 렉시컬 스코프와 렉시컬 클로저와 같은 일부 기능도 차용했다. 공통 Lisp 구현체는 LLVM, Java 가상 머신, x86-64, PowerPC, Alpha, ARM, Motorola 68000, MIPS와 같은 다양한 플랫폼과 Windows, macOS, Linux, Solaris, FreeBSD, NetBSD, OpenBSD, Dragonfly BSD, Heroku와 같은 운영 체제를 대상으로 제공된다.

Scheme은 Guy L. Steele, Jr.와 Gerald Jay Sussman이 발명한 Lisp 프로그래밍 언어의 정적 스코프와 적절한 꼬리 재귀를 지원하는 방언이다. Scheme은 매우 명확하고 간단한 의미론을 지니며, 표현을 형성하는 방법이 적다. 공통 Lisp보다 약 10년 앞서 설계된 Scheme은 더 미니멀한 디자인을 지니며, 표준 기능 세트가 훨씬 적지만, 공통 Lisp에서 지정되지 않은 특정 구현 기능(예: 꼬리 호출 최적화와 완전한 연속성)을 갖춘다. Scheme은 명령형, 함수형, 메시지 전달 스타일을 포함한 다양한 프로그래밍 패러다임을 편리하게 표현할 수 있다. Scheme은 Revised Report on the Algorithmic Language Scheme(수정된 알고리즘 언어 Scheme 보고서)와 Scheme Requests for Implementation(Scheme 구현 요청)과 함께 계속 발전하고 있다.

Clojure는 주로 Java 가상 머신, 공통 언어 런타임(CLR), Python VM, Ruby VM YARV, JavaScript로 컴파일하는 Lisp의 방언이다. Clojure는 실용적인 범용 언어로 설계되었으며, Haskell에서 상당한 영향을 받으며 불변성(immutability)을 매우 강하게 강조한다. Clojure는 Java 프레임워크와 라이브러리에 접근할 수 있도록 하고, 선택적 타입 힌트와 타입 추론을 제공하여 Java 호출에서 반사를 피하고 빠른 원시 작업을 가능하게 한다. Clojure는 다른 Lisp 방언들과의 하위 호환성을 염두에 두고 설계되지 않았다.

또한, Lisp 방언들은 많은 응용 프로그램에서 스크립팅 언어로 사용되며, 그 중 가장 잘 알려진 것은 Emacs 편집기에서 사용하는 Emacs Lisp, AutoCAD에서 사용하는 AutoLISP와 후속 Visual Lisp, Audacity에서 사용하는 Nyquist, 그리고 LilyPond에서 사용하는 Scheme이다. 유용한 Scheme 인터프리터의 작은 크기 덕분에 임베디드 스크립팅에서 특히 인기가 높다. 예를 들어 SIOD와 TinyScheme은 "Script-fu"라는 이름으로 GIMP 이미지 처리기에서 성공적으로 임베디드 되었다. John Harper가 만든 Lisp 인터프리터 LIBREP는 처음에는 Emacs Lisp 언어를 바탕으로 했으며, Sawfish 윈도우 관리자에 임베디드되었다.

표준화된 방언들

Lisp에는 공식적으로 표준화된 방언들이 있다: R6RS Scheme, R7RS Scheme, IEEE Scheme, ANSI Common Lisp, 그리고 ISO ISLISP.

언어 혁신[편집]

폴 그레이엄(Paul Graham)은 리스프가 포트란(Fortran)과 같은 기존 언어들과 구별되는 아홉 가지 중요한 특징을 지닌다고 지적한다:

  • goto에 제한되지 않은 조건문
  • 일급 함수
  • 재귀
  • 변수를 포인터로 균일하게 취급하며, 값에 타입을 둔다
  • 가비지 컬렉션
  • 명령문 없이 오직 표현식으로만 구성된 프로그램
  • 문자열 데이터 타입과 구별되는 심볼 데이터 타입
  • 많은 괄호를 사용하여 기호 트리로 구성된 코드 표기법
  • 로드 타임, 컴파일 타임, 실행 타임에 모두 사용할 수 있는 완전한 언어

리스프는 프로그램 코드 구조가 표준 데이터 구조로 충실하게 표현된 첫 번째 언어로, 이는 훗날 호모아이코닉성(homoiconicity)이라고 불리게 되었다. 따라서 Lisp 함수는 하위 수준의 조작 없이 Lisp 프로그램 내에서 조작, 변경 또는 생성될 수 있다. 이는 언어의 표현력에 있어 주요 장점 중 하나로 여겨지며, 구문 매크로 및 메타순환 평가에 적합한 언어로 만든다.

매카시는 if-then-else 문법을 Fortran으로 작성된 체스 프로그램을 위해 발명했다. 그는 이를 ALGOL에 포함시킬 것을 제안했으나 ALGOL 58 사양에 포함되지 않았다. Lisp에서는 McCarthy가 더 일반적인 cond 구조를 사용했다. ALGOL 60은 if-then-else를 채택하고 이를 널리 퍼뜨렸다.

리스프는 Smalltalk 개발팀의 리더인 Alan Kay에게 깊은 영향을 미쳤으며, 그와 동시에 Lisp는 Smalltalk로부터 영향을 받았다. 이후 방언들에서는 객체 지향 프로그래밍 기능(상속 클래스, 인스턴스 캡슐화, 메시지 전달 등)을 채택했다. Flavors 객체 시스템은 다중 상속과 믹스인(mixin) 개념을 도입했다. 공통 Lisp 객체 시스템은 다중 상속, 다중 디스패치를 갖춘 다형성 메서드, 일급 제네릭 함수를 제공하며, 동적 디스패치의 유연하고 강력한 형태를 만들어낸다. 이는 후속 Lisp(그리고 Scheme) 객체 시스템의 템플릿이 되었으며, 종종 메타객체 프로토콜을 통해 구현된다. Lisp는 Smalltalk에 이어 두 번째로 이러한 메타객체 시스템을 갖춘 언어였다.

Lisp는 자동 가비지 컬렉션 개념을 도입했다. 가비지 컬렉션은 시스템이 힙을 순차적으로 탐색하여 사용되지 않는 메모리를 찾는 방식이다. 현대의 고급 가비지 컬렉션 알고리즘, 예를 들어 세대형 가비지 컬렉션의 발전은 Lisp에서 사용된 덕분에 이루어졌다.

Edsger W. Dijkstra는 1972년 튜링 상 수상 연설에서 다음과 같이 말했다:

"몇 가지 기본 원칙을 바탕으로, Lisp는 놀라운 안정성을 보였다. 또한 Lisp는 사실상 가장 정교한 컴퓨터 응용 프로그램 중 상당수를 실현했다. Lisp는 ‘컴퓨터를 가장 지능적으로 잘못 사용하는 방법’이라고 농담처럼 설명되었다. 나는 이 설명이 큰 칭찬이라고 생각한다. 왜냐하면 그것은 해방의 전모를 전달하며, 이 언어가 우리 가장 재능 있는 동료들이 이전에는 불가능했던 사고를 하도록 도왔다고 전하기 때문이다."

초기 컴퓨터 하드웨어(특히 초기 마이크로프로세서)의 자원 요구사항 때문에 Lisp는 AI 커뮤니티를 제외하고는 Fortran이나 ALGOL에서 파생된 C 언어만큼 대중적이지 않았다. 하지만 복잡하고 동적인 응용 프로그램에 적합한 덕분에, Lisp는 2010년대에 다시 관심을 끌었다.

구문과 의미론[편집]

기호 표현식(S-expressions)[편집]

리스프는 표현식 중심의 언어이다. 대부분의 다른 언어와 달리 "표현식"과 "문" 사이에 구분을 두지 않으며, 모든 코드와 데이터는 표현식으로 작성된다. 표현식이 평가되면 값(여러 값을 포함할 수 있음)을 생성하며, 이 값은 다른 표현식에 삽입될 수 있다. 각 값은 어떤 데이터 타입이든 될 수 있다.

매카시의 1958년 논문에서는 두 가지 종류의 구문을 소개했다: 기호 표현식(S-expressions, sexps)은 코드와 데이터의 내부 표현을 반영하며, 메타 표현식(M-expressions)은 S-expressions의 함수로 표현된다. M-expressions는 인기를 끌지 못했으며, 현재 거의 모든 Lisp는 코드와 데이터를 조작하는 데 S-expressions을 사용한다.

괄호의 사용은 Lisp가 다른 프로그래밍 언어와 구별되는 가장 즉각적인 차이점이다. 이로 인해 학생들은 오랫동안 Lisp에게 Lost In Stupid Parentheses 또는 Lots of Irritating Superfluous Parentheses와 같은 별명을 붙여왔다.[67] 그러나 S-expression 구문은 Lisp의 강력한 기능 중 하나이기도 하다: 구문이 간단하고 일관되어 컴퓨터로 조작하기 용이하다. 하지만 Lisp의 구문은 전통적인 괄호 표기법에 국한되지 않는다. 대체 표기법을 포함하도록 확장될 수 있다. 예를 들어, XMLisp는 Common Lisp 확장으로, 메타객체 프로토콜을 사용하여 S-expression을 확장 가능한 마크업 언어(XML)와 통합한다.

표현식에 의존하는 것은 언어에 큰 유연성을 부여한다. Lisp 함수는 목록으로 작성되므로 데이터처럼 처리될 수 있다. 이를 통해 다른 프로그램을 조작하는 프로그램(메타프로그래밍)을 쉽게 작성할 수 있다. 많은 Lisp 방언은 매크로 시스템을 사용하여 이 기능을 활용하며, 이를 통해 언어의 확장이 거의 무제한으로 이루어질 수 있다.

리스트[편집]

리스프 리스트는 그 요소들이 공백으로 구분되고 괄호로 둘러싸여 작성된다. 예를 들어, (1 2 foo)는 1, 2, foo라는 세 개의 원자(atom)를 요소로 가지는 리스트이다. 이 값들은 암시적으로 타입이 지정된다: 각각 두 개의 정수와 Lisp 고유의 데이터 타입인 "심볼(symbol)"로, 별도로 선언할 필요는 없다.

빈 리스트 ()는 특수한 원자 nil로도 표현된다. 이는 Lisp에서 유일하게 원자이면서 리스트인 엔티티이다.

표현식은 리스트로 작성되며, 접두 표기법(prefix notation)을 사용한다. 리스트의 첫 번째 요소는 함수 이름, 매크로 이름, 람다 표현식, 또는 "특수 연산자"(아래 설명 참조)의 이름이다. 나머지 리스트는 인자들이다. 예를 들어, 함수 list는 인자들을 리스트로 반환하므로, 다음 표현식

(list 1 2 (quote foo))

는 (1 2 foo)라는 리스트로 평가된다. 앞의 예시에서 foo 앞에 있는 "quote"는 "특수 연산자"로, 그 인자를 평가하지 않고 그대로 반환한다. 인용되지 않은 표현식들은 상위 표현식이 평가되기 전에 재귀적으로 평가된다. 예를 들어,

(list 1 2 (list 3 4))

는 (1 2 (3 4))라는 리스트로 평가된다. 세 번째 인자는 리스트이고, 리스트는 중첩될 수 있다.

연산자[편집]

산술 연산자는 비슷한 방식으로 처리된다. 예를 들어,

(+ 1 2 3 4)

는 10으로 평가된다. 중위 표기법에서는 "1 + 2 + 3 + 4"에 해당한다.

Lisp에서는 ALGOL 파생 언어에서 구현된 연산자 개념이 없다. Lisp의 산술 연산자는 가변 인자를 받는 함수(또는 n-ary 함수)로, 아무 개수의 인자도 받을 수 있다. C 스타일의 '++' 증가 연산자는 종종 incf라는 이름으로 구현되며, 구문은

(incf x)

로, 이는 (setq x (+ x 1))에 해당하며 x의 새로운 값을 반환한다.

"특수 연산자"(때때로 "특수 형태"라고도 함)는 Lisp의 제어 구조를 제공한다. 예를 들어, 특수 연산자 if는 세 개의 인자를 받는다. 첫 번째 인자가 nil이 아니면 두 번째 인자를 평가하고, 그렇지 않으면 세 번째 인자를 평가한다. 따라서, 표현식

(if nil
 (list 1 2 "foo")
 (list 3 4 "bar"))

는 (3 4 "bar")로 평가된다. 물론, 여기서 nil 대신 비트리비얼한 표현식이 사용되면 더 유용해진다.

Lisp는 또한 논리 연산자 and, or, not을 제공한다. and와 or 연산자는 단락 평가(short-circuit evaluation)를 하며, 각각 첫 번째 nil 인자와 첫 번째 non-nil 인자를 반환한다.

(or (and "zero" nil "never") "James" 'task 'time)

는 "James"로 평가된다.

람다 표현식과 함수 정의[편집]

또 다른 특수 연산자, lambda는 변수를 값에 바인딩하여 표현식 내에서 평가될 수 있게 한다. 이 연산자는 함수를 생성하는 데에도 사용된다: lambda의 인자는 인자 목록과 함수가 평가되는 표현식들이다(반환 값은 마지막으로 평가된 표현식의 값이다). 예를 들어,

(lambda (arg) (+ arg 1))

는 하나의 인자를 받아 이를 arg에 바인딩하고 그 인자보다 하나 큰 값을 반환하는 함수를 생성한다. 람다 표현식은 명명된 함수와 다르게 처리되지 않으며, 동일한 방식으로 호출된다. 따라서,

((lambda (arg) (+ arg 1)) 5)

는 6으로 평가된다. 여기서는 함수 적용을 하고 있다: 익명 함수를 실행하며 5를 그 값으로 넘겨주고 있다.

명명된 함수는 lambda 표현식을 심볼에 저장하여 defun 매크로를 사용해 생성된다.

(defun foo (a b c d) (+ a b c d))

는 foo라는 이름의 새로운 함수를 전역 환경에 정의한다. 이는 개념적으로 다음과 비슷하다:

(setf (fdefinition 'f) #'(lambda (a) (block f b...)))

여기서 setf는 첫 번째 인자인 fdefinition 'f의 값을 새 함수 객체로 설정하는 매크로이다. fdefinition은 f라는 이름의 함수에 대한 전역 함수 정의이다. #'는 함수 특수 연산자의 약어로, 함수 객체를 반환한다.

원자(Atoms)[편집]

원래의 LISP에서는 두 가지 기본 데이터 타입이 있었다: 원자와 리스트. 리스트는 요소들의 유한한 순서 있는 시퀀스였으며, 각 요소는 원자나 리스트일 수 있었다. 원자는 숫자나 심볼로 이루어져 있었으며, 심볼은 본질적으로 고유한 이름을 가진 항목으로, 소스 코드에서 알파벳과 숫자로 된 문자열로 작성되고, 변수 이름이나 심볼 처리에서 데이터 항목으로 사용되었다. 예를 들어, 리스트 (FOO (BAR 1) 2)는 세 개의 요소를 포함한다: 심볼 FOO, 리스트 (BAR 1), 숫자 2.

원자와 리스트의 본질적인 차이는 원자는 불변적이고 고유하다는 점이다. 소스 코드에서 다른 곳에 나타난 두 원자가 정확히 동일하게 작성되었다면 이는 동일한 객체를 나타내지만, 각 리스트는 다른 리스트와 독립적으로 변경될 수 있는 개별 객체로, 비교 연산자에 의해 구별될 수 있다.

더 많은 데이터 타입이 후속 Lisp 방언에서 도입되고 프로그래밍 스타일이 발전하면서 원자의 개념은 중요성을 잃었다. 많은 방언에서는 호환성 유지를 위해 여전히 atom이라는 술어를 사용하여, cons가 아닌 객체에 대해 true를 반환한다.

컨스(Cons)와 리스트[편집]

Lisp 리스트는 단일 연결 리스트로 구현된다. 이 리스트의 각 셀은 컨스(cons)라고 불리며, 두 개의 포인터인 car와 cdr로 구성된다. 이들은 각각 데이터와 다음 요소를 가리키는 포인터로, 링크드 리스트에서의 데이터 필드와 다음 필드에 해당한다.

컨스로 만들어질 수 있는 데이터 구조 중 가장 기본적인 것은 "정상 리스트"라고 불린다. 정상 리스트는 특수한 nil(빈 리스트) 심볼이거나, car가 데이터(또는 또 다른 cons 구조체, 예를 들어 리스트일 수 있음)를 가리키고, cdr이 또 다른 정상 리스트를 가리키는 cons로 구성된다.

주어진 cons가 연결 리스트의 헤드로 간주될 때, 그 car는 리스트의 첫 번째 요소를 가리키고, cdr은 나머지 리스트를 가리킨다. 이러한 이유로 car와 cdr 함수는 연결 리스트의 cons에서 각각 first와 rest로 불리기도 한다.

따라서 Lisp 리스트는 C++나 Java의 컨테이너 클래스 인스턴스처럼 원자적인 객체가 아니다. 리스트는 다만 연결된 cons들의 집합일 뿐이다. 특정 리스트를 참조하는 변수는 그 리스트의 첫 번째 cons를 가리키는 포인터일 뿐이다. 리스트를 탐색하려면 cdr을 따라가며 각 cons를 방문할 수 있으며, 리스트 위에 함수를 적용하는 고차 함수들로도 탐색할 수 있다.

컨스와 리스트는 Lisp 시스템에서 매우 일반적이기 때문에, 이들이 Lisp의 유일한 데이터 구조라는 오해가 흔하다. 사실, 가장 간단한 Lisp을 제외하고는 대부분 다른 데이터 구조들, 예를 들어 벡터(배열), 해시 테이블, 구조체 등이 있다.

S-표현식 (S-expressions)은 리스트를 나타낸다

괄호로 묶인 S-표현식은 연결 리스트 구조를 나타낸다. 동일한 리스트를 여러 가지 방식으로 표현할 수 있다. cons는 점-쌍 표기법(dotted-pair notation)으로 (a . b)와 같이 작성할 수 있으며, 여기서 a는 car, b는 cdr이다. 더 긴 정상 리스트는 점-쌍 표기법으로 (a . (b . (c . (d . nil))))와 같이 작성될 수 있으며, 이는 리스트 표기법에서는 (a b c d)로 축약된다. 비정상 리스트는 두 가지 표기법의 조합으로 표현될 수 있다. 예를 들어, (a b c . d)는 세 개의 cons를 가지는 리스트로, 마지막 cdr가 d인 경우이다(즉, 완전히 명시된 형태는 (a . (b . (c . d)))이다).

리스트 처리 절차 (List-processing procedures)

Lisp는 리스트를 접근하고 제어하기 위한 여러 내장 절차를 제공한다. 리스트는 list 절차를 사용하여 직접 생성할 수 있으며, 이는 임의의 개수의 인자를 받아 이 인자들의 리스트를 반환한다.

(list 1 2 'a 3)
;Output: (1 2 a 3)


(list 1 '(2 3) 4)
;Output: (1 (2 3) 4)

리스트는 cons 절차를 사용하여 리스트 앞에 요소를 추가할 수 있다. 리스트가 cons 쌍으로 구성되는 방식 때문에, cons 절차는 리스트 인자들을 처리할 때 비대칭적으로 동작한다.


(cons 1 '(2 3))
;Output: (1 2 3)


(cons '(1 2) '(3 4))
;Output: ((1 2) 3 4)

append 절차는 두 개 이상의 리스트를 서로 붙인다. Lisp 리스트는 연결 리스트이므로, 두 리스트를 붙이는 것은 시간 복잡도가 O(n)이다.

(append '(1 2) '(3 4))
;Output: (1 2 3 4)


(append '(1 2 3) '() '(a) '(5 6))
;Output: (1 2 3 a 5 6)

공유 구조 (Shared structure)[편집]

Lisp 리스트는 단순 연결 리스트이기 때문에 서로 구조를 공유할 수 있다. 즉, 두 리스트가 동일한 꼬리나 최종 cons 시퀀스를 가질 수 있다. 예를 들어, 다음 Common Lisp 코드 실행 후:

(setf foo (list 'a 'b 'c))
(setf bar (cons 'x (cdr foo)))

리스트 foo와 bar는 각각 (a b c)와 (x b c)이다. 그러나 꼬리 (b c)는 두 리스트에서 동일한 구조이다. 이는 복사본이 아니며, (b c)를 가리키는 cons 셀은 두 리스트에서 동일한 메모리 위치에 있다.

구조를 공유하는 것은 복사하는 것보다 성능 개선을 가져올 수 있다. 그러나 이 기법은 인자로 받은 리스트를 변경하는 함수와 상호작용할 때 원치 않는 결과를 초래할 수 있다. 예를 들어, 다음 코드는 foo 리스트에서 세 번째 항목을 'goose'로 변경하지만, bar 리스트도 변경된다.

(setf (third foo) 'goose)

이것은 foo를 (a b goose)로 변경하고, 동시에 bar를 (x b goose)로 변경한다 – 이는 예상치 못한 결과일 수 있다. 이러한 함수는 파괴적인 함수(destructive functions)로 문서화되며, 이는 공유된 구조를 변경하는 함수들에 대해 경고하는 이유이다.

함수형 프로그래밍을 선호하는 사람들은 파괴적인 함수들을 피하려 한다. Scheme 방언에서는 함수형 스타일을 선호하며, 파괴적인 함수 이름 뒤에 주의의 느낌표(bang)를 붙인다. 예를 들어, set-car!는 cons의 car를 교체하는 함수이다. Common Lisp 방언에서는 파괴적인 함수들이 일반적이며, set-car!의 동등한 함수는 rplaca로, 이는 "replace car"에서 유래했다.

자체 평가 형식과 인용[편집]

Lisp는 사용자가 입력한 표현식을 평가한다. 심볼과 리스트는 다른(보통 더 단순한) 표현식으로 평가된다. 예를 들어, 심볼은 그것이 나타내는 변수의 값으로 평가되며, (+ 2 3)은 5로 평가된다. 그러나 대부분의 다른 형식은 자기 자신으로 평가된다. 예를 들어, 5를 입력하면 Lisp는 5를 반환한다.

어떤 표현식도 평가되지 않도록 표시할 수 있다(심볼과 리스트에 필요). 이것이 바로 인용(quote) 특별 연산자, 또는 그 약어인 '(하나의 작은따옴표)의 역할이다. 예를 들어, 보통 foo라는 심볼을 입력하면 그에 해당하는 변수의 값을 반환하지만(혹은 해당 변수가 없다면 오류가 발생), 리터럴 심볼을 참조하려면 (quote foo) 또는 보통 'foo를 입력한다.

Common Lisp과 Scheme 모두 백쿼트 연산자(Scheme에서 quasiquote라고 함)를 지원한다. Scheme에서는 이를 '준 인용(quasiquote)'이라고 부르며, 백틱(Backtick) 문자로 입력된다. 이는 평범한 인용과 거의 같지만, 표현식이 평가되고 그 값이 인용된 리스트에 삽입되도록 해주는 ,(언쿼트)와 ,@(스플라이스) 연산자가 추가된 형태이다. 예를 들어, 변수 snue가 값 (bar baz)을 가지면, `(foo ,snue)는 (foo (bar baz))로 평가되고, `(foo ,@snue)는 (foo bar baz)로 평가된다. 백쿼트는 주로 매크로 확장을 정의할 때 사용된다.

자기 평가 형식과 인용된 형식은 Lisp에서 리터럴에 해당한다. 프로그램 코드에서 (변경 가능한) 리터럴의 값을 수정할 수도 있다. 예를 들어, 함수가 인용된 형식을 반환하고, 그 함수를 호출하는 코드가 해당 형식을 수정하면, 이후 호출에서 함수의 동작을 변경할 수 있다.

(defun should-be-constant ()
  '(one two three))
(let ((stuff (should-be-constant)))
  (setf (third stuff) 'bizarre))   ; 나쁜 예!
(should-be-constant)   ; (one two bizarre)를 반환

이와 같이 인용된 형식을 수정하는 것은 일반적으로 나쁜 스타일로 간주되며, ANSI Common Lisp에서는 오류로 정의된다(컴파일된 파일에서 "정의되지 않음" 동작이 발생할 수 있다. 이는 파일 컴파일러가 유사한 상수를 결합하고, 쓰기 보호된 메모리에 배치하는 등의 최적화를 수행하기 때문이다).

Lisp에서의 인용 공식화는 더글러스 호프스태터(《괴델, 에셔, 바흐》)와 다른 이들에 의해 자기 참조라는 철학적 아이디어의 예시로 언급되었다.

범위와 폐쇄[편집]

Lisp 계열 언어는 동적 스코프와 정적(혹은 렉시컬) 스코프의 사용에 대해 나뉜다. Clojure, Common Lisp, Scheme은 기본적으로 정적 스코핑을 사용하며, newLISP, Picolisp, Emacs와 AutoCAD의 내장 언어들은 동적 스코핑을 사용한다. Emacs는 24.1 버전부터 동적 및 렉시컬 스코핑을 모두 사용한다.

프로그램 코드의 리스트 구조- 매크로와 컴파일러의 활용

Lisp와 다른 언어의 중요한 차이점은 Lisp에서 프로그램의 텍스트 표현이 기본적으로 내부 데이터 구조(링크드 리스트, 심볼, 숫자, 문자 등)와 동일하다는 점이다. 이 점을 활용하여 매우 강력한 매크로 시스템을 구현할 수 있다.

C 전처리기에서 정의된 매크로처럼, Lisp 매크로도 코드를 반환하여 컴파일할 수 있다. 그러나 C 전처리기 매크로와는 달리, Lisp 매크로는 Lisp 함수이므로 Lisp의 모든 기능을 활용할 수 있다.

또한 Lisp 코드의 구조가 리스트와 동일하기 때문에, 매크로는 언어 내의 모든 리스트 처리 함수를 사용하여 만들 수 있다. 즉, Lisp가 데이터 구조에 대해 할 수 있는 모든 작업을 Lisp 매크로도 코드에 대해 할 수 있다. 반면 대부분의 다른 언어에서는 파서의 출력물이 언어 구현 내에서만 사용되며, 프로그래머가 이를 조작할 수 없다.

이 기능은 언어 내에서 효율적인 언어를 개발할 수 있게 해준다. 예를 들어, Common Lisp 객체 시스템(CLOS)은 매크로를 사용하여 언어 확장으로 깔끔하게 구현될 수 있다. 이는 애플리케이션이 다른 상속 메커니즘을 필요로 할 때, 다른 객체 시스템을 사용할 수 있다는 뜻입니다. 대부분의 다른 언어들과는 대조적으로, 예를 들어 Java는 다중 상속을 지원하지 않으며, 이를 추가하는 합리적인 방법이 없다.

간단한 Lisp 구현에서는 이 리스트 구조가 프로그램 실행을 위해 직접 해석되며, 함수는 실제로 리스트 구조의 일부로, 인터프리터가 이를 실행하면서 탐색한다. 그러나 대부분의 주요 Lisp 시스템은 컴파일러를 포함하고 있다. 컴파일러는 리스트 구조를 기계어 또는 바이트코드로 변환하여 실행한다. 이 코드는 C와 같은 전통적인 언어에서 컴파일된 코드만큼 빠르게 실행될 수 있다.

매크로는 컴파일 단계 전에 확장되므로 흥미로운 옵션을 제공한다. 예를 들어, 프로그램에 미리 계산된 테이블이 필요하면, 매크로가 컴파일 시간에 테이블을 생성할 수 있다. 그러면 컴파일러는 테이블만 출력하고 실행 시간에 테이블을 생성하는 코드를 호출할 필요가 없다. 일부 Lisp 구현에서는 eval-when 메커니즘을 제공하여, 코드가 컴파일 시간에 필요하지만, 최종 모듈에 포함되지 않도록 할 수 있다.

평가 및 읽기–평가–출력 루프[편집]

Lisp 언어는 종종 인터랙티브한 명령줄과 함께 사용되며, 이는 통합 개발 환경(IDE)과 결합될 수 있다. 사용자는 명령줄에서 식을 입력하거나, IDE에 이를 Lisp 시스템으로 전달하도록 지시한다. Lisp는 입력된 식을 읽고, 이를 평가한 후 결과를 출력한다. 이 때문에 Lisp 명령줄은 읽기–평가–출력 루프(READ–EVAL–PRINT LOOP, REPL)라고 불린다.

REPL의 기본 작동은 다음과 같다. 이는 실제 Lisp의 많은 요소들, 예를 들어 quoting과 macros 등을 생략한 단순한 설명이다.

read 함수는 텍스트 S-표현식을 입력으로 받아 내부 데이터 구조로 구문 분석한다. 예를 들어, 프롬프트에 (+ 1 2)를 입력하면 read는 이를 세 개의 요소를 가진 연결 리스트로 변환한다: 기호 +, 숫자 1, 숫자 2. 이 리스트는 또한 유효한 Lisp 코드이다. 즉, 평가될 수 있다. 이유는 이 리스트의 car가 함수 이름을 나타내기 때문이다. 여기서는 덧셈 연산이다.read(+ 1 2)read+

foo는 단일 기호로 읽힌다. 123은 숫자 123으로 읽힌다. "123"은 문자열 "123"으로 읽힌다.foo123"123"

eval 함수는 데이터를 평가하여 결과로 하나 이상의 Lisp 데이터를 반환한다. 평가가 반드시 해석을 의미하는 것은 아니며, 일부 Lisp 시스템에서는 모든 식을 네이티브 기계 코드로 컴파일한다. 그러나 평가를 해석으로 설명하는 것은 간단하다: 만약 리스트의 car가 함수 이름을 갖고 있다면, eval은 먼저 그 리스트의 cdr에 있는 각 인자를 평가하고, 그 다음에 함수에 인자들을 적용한다. 이 경우 함수는 덧셈이며, 이를 인자 목록 (1 2)에 적용하면 결과 3이 나온다. 이것이 평가의 결과이다.evaleval(1 2)3

기호 foo는 기호 foo의 값을 평가한 결과로 반환된다. 문자열 "123" 같은 데이터는 동일한 문자열로 평가된다. 리스트 (quote (1 2 3))는 리스트 (1 2 3)로 평가된다.foo(quote (1 2 3))

print 함수는 사용자가 볼 수 있도록 출력을 나타내는 역할을 합니다. 결과가 3처럼 간단하면 이는 간단히 출력된다. 리스트 구조처럼 평가된 식은 print가 리스트를 순회하며 S-표현식으로 출력할 것이다.print3print

Lisp REPL을 구현하려면, 이 세 가지 함수와 무한 루프 함수만 구현하면 된다. (물론 eval의 구현은 복잡할 수 있으며, if나 lambda 같은 특수 연산자도 구현해야 합니다.) 이 작업이 완료되면, 기본적인 REPL은 한 줄의 코드로 구현할 수 있다:evaliflambda (loop (print (eval (read)))).

Lisp REPL은 일반적으로 입력 편집, 입력 기록, 오류 처리 및 디버거 인터페이스도 제공한다.

Lisp는 보통 열렬히 평가된다. Common Lisp에서는 인자들이 적용 순서대로(leftmost innermost) 평가되며, Scheme에서는 인자의 순서가 정의되지 않아 컴파일러가 최적화를 할 수 있는 여지를 남긴다.

제어 구조[편집]

Lisp는 원래 매우 적은 수의 제어 구조를 갖고 있었지만, 언어의 발전과 함께 훨씬 더 많은 제어 구조가 추가되었다. (Lisp의 원래 조건 연산자 cond는 나중에 등장한 if-then-else 구조의 선구자이다.)

Scheme 방언을 사용하는 프로그래머들은 종종 꼬리 재귀(tail recursion)를 사용하여 반복문을 표현한다. Scheme이 학문적 컴퓨터 과학에서 널리 사용되었기 때문에 일부 학생들은 꼬리 재귀가 Lisp에서 반복문을 작성하는 유일한 방법이거나 가장 일반적인 방법이라고 생각할 수 있지만, 이는 잘못된 생각이다. 대부분의 잘 알려진 Lisp 방언들은 명령형(iterative) 스타일의 반복문을 지원한다. Scheme의 do 루프부터 Common Lisp의 복잡한 loop 표현식까지 다양한 반복문이 있다. 또한, 이 문제는 주관적인 문제가 아니라 객관적인 문제입니다. Scheme은 꼬리 호출 처리에 대해 특정 요구사항을 가지고 있기 때문에, Scheme에서 꼬리 재귀 사용이 일반적으로 권장되는 이유는 언어 정의에서 이를 명시적으로 지원하기 때문이다. 반면 ANSI Common Lisp은 꼬리 호출 제거(tail call elimination) 최적화를 요구하지 않기 때문에, Common Lisp에서는 꼬리 재귀 스타일을 더 전통적인 반복문 표현식(do, dolist, loop 등)으로 대체하는 것을 권장하지 않는다. 이는 단순한 스타일 선호의 문제가 아니라, 효율성(예: Common Lisp에서는 명백한 꼬리 호출이 간단한 점프 명령으로 컴파일되지 않을 수 있음)과 프로그램의 정확성(꼬리 재귀가 스택 사용을 증가시켜 스택 오버플로우를 일으킬 수 있음)과 관련된 문제일 수 있다.

일부 Lisp 제어 구조는 특별 연산자(special operators)로, 다른 언어의 문법 키워드와 동등하다. 이러한 연산자를 사용하는 표현식은 함수 호출처럼 보이지만, 인자가 반드시 평가되지 않거나(반복문 표현식의 경우, 인자가 여러 번 평가될 수 있음) 다를 수 있다.

대부분의 다른 주요 프로그래밍 언어와 달리, Lisp는 언어 자체를 사용하여 제어 구조를 구현할 수 있다. 여러 제어 구조는 Lisp 매크로로 구현되며, 매크로 확장을 통해 그 동작을 알고 싶은 프로그래머가 직접 확장할 수 있다.

Common Lisp와 Scheme 모두 비지역 제어 흐름을 위한 연산자를 제공한다. 이 연산자들 간의 차이는 두 방언 간의 가장 깊은 차이점 중 하나이다. Scheme은 call/cc 절차를 사용하여 재진입 가능한 연속(continuation)을 지원하며, 이는 프로그램이 실행 중 특정 지점을 저장하고 나중에 복원할 수 있게 해준다. 반면 Common Lisp은 재진입 가능한 연속을 지원하지 않지만, 여러 가지 방식으로 탈출 연속(escape continuation)을 처리할 수 있다.

종종 동일한 알고리즘은 Lisp에서 명령형 스타일이나 함수형 스타일로 표현할 수 있다. 앞서 언급한 대로, Scheme은 함수형 스타일을 선호하며, 꼬리 재귀와 연속을 사용하여 제어 흐름을 표현한다. 그러나 명령형 스타일도 여전히 가능하다. 많은 Common Lisp 프로그래머들이 선호하는 스타일은 C와 같은 구조적 언어에 익숙한 프로그래머들에게 더 친숙할 수 있으며, Scheme의 스타일은 Haskell과 같은 순수 함수형 언어에 더 가까운 형태이다.

Lisp는 초기의 리스트 처리 전통 덕분에 시퀀스를 순회(iteration)하는 고차 함수들이 풍부하게 존재한다. 다른 언어들에서 명시적인 반복문(예: C의 for 루프)이 필요할 경우에도, Lisp에서는 고차 함수로 같은 작업을 처리할 수 있는 경우가 많다. (이것은 많은 함수형 프로그래밍 언어들에도 해당된다.)

좋은 예는 Scheme에서는 map이라고 불리고 Common Lisp에서는 mapcar라고 불리는 함수입니다. 함수와 하나 이상의 리스트를 주면, mapcar는 각 리스트의 요소에 함수가 순차적으로 적용되며, 그 결과를 새로운 리스트로 모은 결과를 반환합니다:

(mapcar #'+ '(1 2 3 4 5) '(10 20 30 40 50))

이 코드는 + 함수가 각각의 리스트 요소 쌍에 대해 적용되어 결과 (11 22 33 44 55)를 반환한다.

예시[편집]

다음은 Common Lisp 코드의 예시이다.

기본적인 "Hello, World!" 프로그램:

(print "Hello, World!")

Lisp 문법은 자연스럽게 재귀를 잘 지원한다. 재귀적으로 정의된 집합의 열거와 같은 수학적 문제를 이 표기법으로 쉽게 표현할 수 있습니다. 예를 들어, 숫자의 팩토리얼을 계산하는 코드:

(defun factorial (n)
   (if (zerop n) 1
       (* n (factorial (1- n)))))

위의 구현을 꼬리 재귀를 최적화하는 Lisp 시스템에서는 스택 공간을 덜 사용하는 대체 구현이 가능하다:

(defun factorial (n &optional (acc 1))
   (if (zerop n) acc
       (factorial (1- n) (* acc n))))

위의 예제들과 비교할 때, Common Lisp의 loop 매크로를 사용한 반복문 버전은 다음과 같다:

(defun factorial (n)
   (loop for i from 1 to n
       for fac = 1 then (* fac i)
       finally (return fac)))

다음 함수는 리스트를 반전시키는 예시이다. (Lisp의 내장 함수 reverse도 동일한 작업을 수행한다.)

(defun -reverse (list)
   (let ((return-value))
     (dolist (e list) (push e return-value))
     return-value))

객체 시스템[편집]

Lisp 위에 또는 그와 함께 구축된 다양한 객체 시스템 및 모델들이 있다. 그 중 일부는 다음과 같다:

  • Common Lisp Object System (CLOS): ANSI Common Lisp의 필수적인 부분으로, New Flavors와 CommonLOOPS에서 유래한 것이다. ANSI Common Lisp은 1994년에 객체 지향 프로그래밍 언어로 최초로 표준화된 언어였다 (ANSI X3J13).
  • ObjectLisp 또는 Object Lisp: Lisp Machines Incorporated와 초기 버전의 Macintosh Common Lisp에서 사용되었다.
  • LOOPS (Lisp Object-Oriented Programming System)와 그 후속인 CommonLoops.
  • Flavors: MIT에서 개발되었고, 그 후속 버전인 New Flavors는 Symbolics에서 개발되었다.
  • KR (Knowledge Representation): Garnet이라는 GUI 라이브러리를 작성하기 위해 개발된 제약 기반 객체 시스템이다.
  • Knowledge Engineering Environment (KEE): UNITS라는 객체 시스템을 사용하며, 추론 엔진과 진리 유지 시스템(ATMS)을 통합했다.

운영 체제[편집]

Lisp를 기반으로 한 운영 체제들, 또는 Lisp를 사용하여 작성된 운영 체제들이 여러 가지 있다. 이들 운영 체제는 Lisp의 특징, 규약, 방법, 데이터 구조 등을 활용하거나 Lisp로 작성된 것이다. 그 예시는 다음과 같다:

  • Genera (이후 Open Genera로 이름 변경): Symbolics에서 개발한 운영 체제로, Lisp 기반의 그래픽 환경을 제공한다.
  • Medley: Interlisp로 작성된 운영 체제로, 원래 Xerox의 Star 워크스테이션에서 실행되는 그래픽 기반 운영 체제의 한 가족이었다.
  • Mezzano: Lisp로 작성된 또 다른 운영 체제.
  • Interim: Lisp를 사용하여 작성된 임시 운영 체제이다.
  • ChrysaLisp: Tao Systems의 TAOS를 개발한 개발자들이 만든 Lisp 기반 운영 체제이다.
  • Guix System: GNU/Linux를 위한 시스템으로, Lisp의 Guile을 기반으로 관리되는 운영 체제이다.

이들 운영 체제는 각각 특정 목적과 환경에 맞게 Lisp의 고유한 특성을 활용하여 설계되었다.

참고자료[편집]

같이 보기[편집]


  검수요청.png검수요청.png 이 리스프 문서는 인공지능 역사에 관한 글로서 검토가 필요합니다. 위키 문서는 누구든지 자유롭게 편집할 수 있습니다. [편집]을 눌러 문서 내용을 검토·수정해 주세요.