Scheme
(display "Hello, world!")(newline)
1. 개요
Lisp 계열의 언어 중 코먼 리스프(Common Lisp)와 함께 가장 유명하고 대표적인 프로그래밍 언어이다. 다른 리스프 계열[1] 보다 함수형 프로그래밍을 적극 장려하는 언어로, 언어 표준에 순수 함수형에서 벗어나는 함수 이름 뒤에는 꼭 "!"를 붙이는 것을 관례로 명시할 정도이다.[2] 그러나 그보다 더 핵심적인 특징은 최소한의 기본 명령만 기계어로 정의가 되어있고, 언어 표준에 명시된 다른 명령들은 기본 명령에 속한 람다(lambda)함수를 이용해 정의가 되어있다는 것이다. 이 때문에 스킴은 다른 프로그래밍 언어에 비해 매우 사이즈가 작으면서도, 기본만 사용해 필요에 따라 무엇이든 정의해 사용할 수 있는 독특한 디자인을 가지고 있다. 커먼 리스프를 109가지 기능이 붙어있는 다목적 공구에 비유하자면, 스킴은 딱 최소한의 10개 기능만 붙어있는 맥가이버 칼에 비유할 수 있다. 같은 뿌리에서 나온 다른 사이즈의 언어라는 점에서 C++와 C의 관계와도 비슷할지도 모르겠지만, C++의 기능을 C에서 구현하려면 오만가지 삽질을 해야하는데 비해 스킴은 커먼 리스프의 기능을 구현 가능한, Greenspun's Tenth Rule[3] 에서 가장 안전한 언어라 할 수 있다.
그 시초는 칼 휴잇(Carl Hewitt)의 액터(Actor) 모델을 이해하기 위한 시도에서 비롯되었다. 제럴드 서스먼(Gerald Sussman)과 가이 스틸 주니어(Guy Steele Jr)가 Maclisp에 기반한 작은 리스프 인터프리터를 만들면서 액터 모델을 구현하고 메시징 기능을 추가한 것이 바로 Scheme의 시초이다. 1975년 당시 이름은 Schemer였는데, 이는 Plannar, Conniver등 리스프에서 파생된 다른 언어의 이름이 ~er로 끝나서 이를 따라 명명했다고 한다. 이후 개발자들이 파일이름과 확장자가 각각 6글자로 제한되는 ITS 운영체제를 사용하면서 현재의 이름인 Scheme으로 이름이 변경되었다.
당시에는 프로그램에서 함수를 불러올 때 호출하고 끝나서 돌아오는 과정에서 시간이 오래 걸려서 많은 수의 함수를 쓰는 것은 비효율적이라는 것이 상식이었다.[4] 서스먼과 스틸이 하던 연구는 이 "상식"을 뒤집는 함수 호출 구조에 대한 것으로, '''함수를 호출하고 값을 반환하는 대신 주어진 환경에서 메모리 주소를 점프하며 함수만 갈아끼우는''' 획기적인 방식을 구현하기 위한 프로토타입 언어였다. 둘은 처음에 (이후에 나온) 커먼 리스프와 같은 규모의 언어를 생각하고 있었지만 정작 나온 것은 극도의 미니멀리즘에 기반한 언어였다.
여기서 스킴의 행보가 여러 방향으로 찢어지게 되는데, 대표적으로 극단적으로 간소하면서도 이해하기 쉬운 언어를 교육용으로 사용하려는 방향은 서스먼의 명저 SICP(Structure and Interpretation of Computer Programs)에서 드러난다. 이 책은 MIT 컴퓨터 과학 전공의 동명의 입문 과목 교과서로 유명해졌고, 한동안 미국 전역에서 이를 따라 SICP와 스킴으로 입문을 가르치는 학교가 꽤 있었다.[5][6] 반면에 한편에서는 이 우아한 언어를 실제로 업무에 사용해보려는 노력으로, R5RS 표준의 제정 이후 SRFI(Scheme Request for Implementation, 스킴 구현 요청)을 통해 프로그래머들이 필요한 기능들을 요청하고 직접 구현하는 움직임이 활발해졌다. 간단한 구조 때문에 성능을 생각 않는다면 취미삼아 만들어볼만한 사이즈의 언어였기에 각종 컴파일러와 인터프리터가 우후죽순처럼 생겨났고, 각각의 부분은 호환되는 부분이 많지만 세세한 데에서 호환되지 않는 경우가 꽤 많았다.
이러한 분열의 와중에서 스킴에 가장 큰 타격을 준 것은 R6RS 표준의 제정이었다. 원래는 양쪽의 입맛을 모두 맞추기 위해 각종 기능을 넣은 새 표준이었지만, R5RS와 호환되지 않는데다 R5RS 표준에 비해 몇 배나 큰 기본 베이스에 교육자들은 사용을 거부했고, 기존에 사실상의 표준으로 사용되던 SRFI 라이브러리와 전혀 호환되지 않는 기능에 프로그래머들도 사용을 거부했다. 이 때문에 교육용 R5RS, 업무용 R5RS+SRFI, 업무용 R6RS로 스킴 커뮤니티는 완전히 분열해버렸다.
결국 스킴 제정 위원회는 R7RS 표준을 교육용 기본 언어와 이와 호환되는 업무용 방대한 언어로 나누어 두 표준을 따로 제정하기로 했다. 얼마 전에 새로 제정된 R7RS-small 표준은 R6RS를 완전히 흑역사로 묻어버리고 R5RS기반으로 만들어졌으며, 이후에 제정될 R7RS-large는 커먼 리스프보다 큰 규모의 언어를 목표로 삼는다는데... 글쎄, 위원회 언어는 듀크 누켐 포에버보다 나오기 힘들다. 어차피 SRFI 상당수를 공식으로 지정하는 것이 중점이 될 것이라 하니 SRFI가 잘 지원되는 스킴을 찾아보자.
2. 특징
위에 말했듯이 스킴은 워낙 표준에 따라 다양한 구현이 있고, 그마저도 표준은 그저 대충 거의 다 지키면 되는 것. 수준의 인식이 있어서 각각의 구현이 조금씩 다른 언어나 방언이라고 할 수 있다. 관계가 조금 먼 Racket을 제외하면, 스킴을 스킴으로 묶는 특성은 다음과 같다.
- Lambda: 스킴의 뿌리인 람다식. (lambda (x) ...)로 스킴에 존재하는 거의 모든 것을 나타낼 수 있다. 람대 대수에서의 그 람다 맞다.
- Tail-call elimination (TCE): 위에서 나왔듯 "함수간에 값을 주고받는 형식"이 아닌 "값을 환경으로 두고 함수를 점프해서 갈아끼우는 방식"을 일반화한 것으로, 함수 A가 B를 부를 때 B를 부르는 장소가 Tail[7] 인 경우 그냥 B의 주소로 점프해버리는 방식이다. 하는 법을 알면 재귀적 함수를 부를 때 절대 스택 오버플로우가 나지 않으며 함수 호출 속도도 빠른 스킴의 전매특허 특징으로, 언어 상세에 "tail-call elimination을 하지 않으면 스킴이 아니다"라고 명시되어 있을 정도다. 이후에 많은 언어(주로 함수형) 에서 채용했다.[8]
- Continuation: TCE과 함께 연구의 부산물로, TCE가 함수에 대한 설명이라면 Continuation은 환경에 대한 설명이다. 함수가 서로를 호출하고 자리를 넘겨줄 때 주변 환경을 같이 연속적(continuous)으로 넘겨준다는 의미로, 핵심 call/cc[9] 라는 명령어는 "현재 환경을 기억했다가 새 값을 넘겨받으면 이 자리로 즉시 돌아와서 값을 전달해라"라는 뜻으로 쓰인다. 실 용례로는 간단하게는 return, break 등의 대용품, 복잡하게는 멀티태스킹 관리(!) 등이 있다. 이해하기 복잡하면 C의 setjmp/longjmp의 업그레이드 버전이라 생각하면 된다.
- S-expression: 리스프라면 빼놓을 수 없는 문법 구조로, atom(심볼, 문자, 문자열, 숫자 등)과 괄호 리스트 딱 두개로만 이루어져있어 코드 (car a)와 데이터 '(car a)의 구조가 똑같고 바꿔치기도 가능하다. 이를 Homoiconicity라고 한다.
- Macro: 바로 그 바꿔치기를 가능하게 하는 일등공신으로, 기본 매크로는 커먼 리스프의 매크로보다 덜 강력하지만 오류가 적고 간편한 문법을 사용하며[10] , 대부분의 스킴은 자체적으로 커먼 리스프 수준의 매크로도 함께 지원한다.
3. 종류
워낙 종류가 많아서 고르기 힘들지만 가장 특징적이고 메이저한 스킴은 다음과 같다.
- Chicken: 가장 커뮤니티가 활성화된 R5RS 스킴으로, Freenode IRC의 #Chicken에는 언제나 많은 사람이 상주해있고 즉각 질문에 대한 답이 올라온다. C를 통해 컴파일하며 가장 많은 수의 SRFI와 라이브러리를 지원한다. 특징은 구현방식으로, 새 함수로 넘어갈때 리턴 없이 함수를 계속 부르다 스택이 꽉 차면 GC를 부르고 처음에 시작한 함수로 longjmp를 해버린다. 여기 나열된 스킴 중 유일하게 R7RS 지원 의지를 표현한 스킴. 교육이 아닌 실제 업계에서 사용될 것을 염두에 두고 만들어진 컴파일러/인터프리터이다.
- Gambit: 가장 속도가 빠른 편인 R5RS 스킴. 다만 SRFI와 라이브러리 지원은 상대적으로 좋은 편은 아니다. LambdaNative라는 프레임워크를 사용하면 스킴으로 iOS와 안드로이드 개발이 가능하다(!). C를 통해 컴파일한다.
- Guile GNU 공식 스크립트 언어(R5RS/R6RS 호환). 특징은 GNU와 리눅스 계열의 시스템(혹은 거기서 파생된) 라이브러리가 많다. 2020년 1월 16일 릴리즈한 3.0부터 JIT를 지원한다! 성능은 2.2 버전 대비 평균 2배 향상되었으며 일부 벤치마크 항목은 최대 32배까지 올라간다고 한다. #
- Stalin: 약 빤 네이밍에 걸맞게 약 빤 성능. C를 통해 컴파일하는데, 수치계산에 한해 사람이 짠 C보다 빠른 수준을 넘어서 포트란 수준이다. 단 R4RS인데다 컴파일하는데 조금 큰 파일 하나에 반나절 걸린다.
- Chez Scheme 인디애나 대학의 한 교수가 만든 컴파일러로, Common Lisp 쪽의 LispWorks 에 대응하는 고가의 상용 컴파일러였으나 2016년 4월 27일부로 9.4 버젼을 GitHub 에 오픈소스로 공개했다. # 이전까지는 Petite Chez Scheme 이라는 인터프리터의 바이너리만 무료로 공개했다. LispWorks 와 같이 여러가지 지원도 좋은편이며 (물론 이 지원을 받으려면 유료로 구매해야 한다.) Scheme 컴파일러중에서는 대적할자가 없을정도로 퍼포먼스가 압도적이다. 아래 나오는 Racket 이 PLT-scheme 이던 시절 test-suite 을 돌렸을때 10분이 넘게 걸리던 프로그램이 chez scheme 로 컴파일하니 컴파일 속도도 훨씬 빨랐으며, 실행속도에서도 같은 test-suite 을 30초컷 했다는 유저 경험담도 있을정도. 물론, PLT-scheme 가 퍼포먼스로 유명한 컴파일러는 아니었지만 타 오픈소스 Scheme 컴파일러와 비교하여 퍼포먼스가 딱히 크게 떨어지는 수준은 아니었다는 것을 감안한다면 대충 상상이 될 것이다.
- Ikarus: R6RS 를 지원하는 오픈소스 scheme 컴파일러로, 특이하게도 위 chez scheme 를 만든 교수에게서 박사과정을 했던 사람이 만든 컴파일러이다. 퍼포먼스는 상당한 수준이지만, 개인사정으로 인해 홈페이지도 접속불가이고 현재는 업데이트가 되고있지 않는 상황.
- Racket 스킴이기를 그만 둔 스킴. 딱히 어느 표준에 맞추지 않고 매크로와 확장성을 이용해 어떤 언어든[11] 모방 가능하다. 원래는 plt-scheme이라 불렸지만 지속적으로 스킴의 틀에서 벗어나다 결국 이름을 바꾸었다. (더 자세한 내용은 Racket 항목을 참고하자.)
4. 기타
브랜든 아이크가 넷스케이프 시절 브라우저 내에 동적 웹페이지 구현하기 위해 스크립트 언어로 쓰려 한 게 바로 이 언어였다. 그러나 넷스케이프에서 일반인들도 웹페이지 개발 쉽게 할 수 있도록 하자고 하며 C, Java와 비슷한 문법의 언어로 만들어야 한다고 요구해서 어쩔 수 없이 Scheme을 쓰려는 걸 포기하고 JavaScript라는 새로운 언어를 만들게 된다. 만약 본래 계획대로 스킴을 썼다면 웹은 어떻게 발전했을지, 웹프로그래밍 입문자가 많았을지, 현재 자바스크립트가 안고 있는 여러 문제점들은 생기지 않았는지, 그에 따른 제이쿼리나 리액트, TypeScript 같은 것들이 생겨날 필요가 없었는지 모를 일이다.
[1] Clojure 제외[2] 예를 들어 (set! a 1)은 이미 만들어진 이름 a에다가 새로 1을 배정한다.[3] "Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp." 복잡한 C나 포트란 프로그램은 커먼 리스프에 이미 기본적으로 있는 기능을 직접 만들어 써야 할 가능성이 높은데, 큰 기능들을 여러 개 구현하려다보니 대충 끼워맞추고 상세도 불분명해 버그가 넘쳐나고 느릴 수밖에 없다는 말이다.[4] 그 때문에 GCC같은 C 컴파일러는 작은 함수를 여러개 호출하는 부분을 파악해 하나의 큰 함수로 합쳐버리고, 이를 inlining이라고 한다.[5] 현재는 MIT도 Python으로 갈아탔다.[6] 국내에도 카이스트, 고려대, 동아대 등 몇몇 학교가 교육했다[7] B를 부른 후에 할 것이 return밖에 없는 경우[8] GCC에서 -O2를 하면 C에서도 제약이 많지만 TCE가 된다![9] call-with-current-continuation[10] "..." 점 3개로 뭐든지 패턴매칭이 가능하다![11] 알골 에뮬레이터가 내장되어있다!