Uploaded avatar of iHiD

Summer of Sexps가 시작됐어요!

@iHiD
3년 초과 전
동영상

들어가며

안녕하세요, 여러분! 모두 잘 지내고 계시죠?

Exercism에서는 최근 몇 주 동안 정말 신나는 일이 많았어요. Exercism Premium과 Exercism Insiders를 출시했거든요. 멋진 커뮤니티 콜도 여러 번 있었고, 사이트에 많은 개선 사항을 배포했으며 곧 더 많이 선보일 예정이에요. 지금 기대할 일이 참 많지만, 그중에서도 #12in23의 6번째 달에 들어선 것만큼 신나는 일은 없죠! S-표현식의 여름, 줄여서 멋지게 Summer of Sexps라고 부르는 달이에요.

늘 그렇듯이 프로그래밍 세계의 현명한 현자 Erik이 함께해요.

이번 달에는 다섯 개 언어가 있어요: Clojure, Common Lisp, Emacs Lisp, Racket, Scheme이에요. 이 언어들은 모두 Lisp 언어의 방언이에요. 그래서 이 영상에서는 각 언어의 차이에 너무 집중하기보다 Lisp 자체와 Lisp를 특별하게 만드는 요소를 조금 더 살펴볼 거예요. 그다음에는 각 언어를 간단히 훑어보며 마무리하죠.

그런데 그전에 잠깐 안내할 게 있어요! Summer of Sexps 배지를 받으려면 6월 동안 이 언어 중 하나로 연습 문제 다섯 개를 완료해야 해요.

배지

1년 내내 진행되는 12in23 배지도 있어요. 이 배지를 받으려면 해당 언어로 우리가 선정한 연습 문제 다섯 개를 풀어야 해요. 6월 이후에 이 글을 보고 있다면, 이 부분은 연중 언제든 할 수 있으니 놓쳤다고 걱정하지 않아도 돼요. 많은 사람이 Lisp를 한 번도 다뤄본 적이 없을 테니, Lisp 언어가 어떤 모습인지 맛볼 수 있는 비교적 간단한 연습 문제를 골랐어요.

  • Leap: 불리언 조건과 참/거짓 판정을 다뤄요(선택적으로 렉시컬 스코프도요)
  • Two-Fer: 문자열을 포맷하고 선택적 매개변수를 다뤄요
  • Difference of Squares: 사용자 정의 함수를 호출하고 전위 표기법으로 계산해요
  • Robot Name: 무작위성, atom, 구조화된 데이터를 다뤄요
  • Matching Brackets: 재귀로 문자열을 검증해요

이 연습 문제들과 이전 달의 연습 문제는 모두 #12in23 페이지에서 볼 수 있어요.

개요

자, Lisp 기반 언어들이에요. 우선 Lisp에 대해 조금 이해하는 것부터 시작해야겠죠. Lisp 전반에 대한 간단한 소개로 시작해볼까요.

Lisp

  • 먼저 Lisp는 가장 오래된 언어 중 하나라는 점이에요.

  • 1958년 MIT의 John McCarthy가 만들었어요. 컴퓨터가 방 하나를 위아래로 꽉 채우던 시절이었죠 🙂

  • Lisp라는 이름은 LISt Processing(또는 LISt Processor)의 줄임말로, 리스트 자료 구조의 중요성을 보여줘요.

  • AI 연구를 목적으로 설계됐어요.

  • Lisp는 Alonzo Church가 만든 람다 대수를 기반으로 했어요. 람다 대수는 수학에서 계산을 기술하는 형식 체계예요(간단히 말하면요).

  • 여러 이유로 엄청난 영향을 끼친 언어예요:

  • 지금도 널리 쓰이는 고급 프로그래밍 언어 중 두 번째로 오래됐어요(Fortran 다음으로요)

  • 최초의 고급 함수형 프로그래밍 언어이며, 오늘날 함수형 프로그래밍 하면 떠올리는 많은 기능을 처음 선보였어요.

  • Lisp가 명령형 프로그래밍도 지원했다는 점을 기억해두세요.

  • 가비지 컬렉터를 갖춘 최초의 언어로, 개발자가 직접 메모리를 관리할 필요가 없게 해줬어요.

  • 문법이 비교적 적고 의미 체계가 비교적 단순해서 교육용으로 아주 좋아요.

  • 그래서 Lisp(정확히는 그 방언 중 하나)는 프로그래밍을 가르칠 때 자주 쓰여요.

  • 아주 많은 언어 방언을 만들어냈고(지금도 만들고 있어요!), 그중 Exercism에서 지원하는 것들을 이 글에서 다룰 거예요.

  • 다시 말해, 프로그래밍 언어의 계보에는 C 계열 언어의 갈래가 있듯이 Lisp 계열 언어의 별도 갈래가 있어요.

최근에 Haskell을 만든 사람 중 한 명인 Simon Peyton Jones와 이야기할 기회가 있었는데, 그는 튜링 기계를 중심으로 만들어진 언어와 람다 대수를 중심으로 만들어진 언어의 차이에 대해 얘기했어요. 이 주제가 더 궁금하다면 그 인터뷰를 한번 보길 권해요.

괄호

Lisp에는 괄호가 아주 많아요. 하지만 그게 꼭 나쁜 건 아니에요(C 계열 언어에서 중괄호가 많다고 꼭 나쁜 게 아닌 것처럼요). Lisp는 S-표현식(S-expression)이라는 것을 중심으로 해요. S-표현식은 symbolic expression의 줄임말이고 sexpr 또는 sexp로 더 줄여 쓰는데, 이번 달 챌린지 이름도 여기서 나왔죠. 데이터를 표현하는 식이에요. 원조 Lisp 언어를 위해 만들어졌고, 그 언어 덕분에 널리 퍼졌어요. S-표현식은 두 가지 형태 중 하나예요:

  • atom(예: 'x'). 중첩되지 않은 '값'이나 트리의 잎이라고 생각하면 돼요.
  • x . y 형태의 식. 여기서 x와 y는 S-표현식이에요. 이걸 쌍이라고 생각하면 되는데, y는 리스트의 다음 원소(있다면)이거나 트리의 노드예요. 이 정의가 재귀적이며 잎 수준에서 끝난다는 점에 주목하세요. 보통 이런 형태의 S-표현식에는 괄호를 써요.

S-표현식

S-표현식은 Lisp에서 데이터와 리스트를 모두 표현하는 데 쓰여요. 그래서 리스트를 정의할 때마다 괄호를 쓰게 돼요. 여기에 다음 사실까지 더하면: 리스트는 Lisp의 핵심 자료 구조이고(이름도 거기서 나왔죠), 일부 Lisp에서는 유일한 자료 구조이며, 결국 괄호가 아주 많아지게 돼요. 리스트가 얼마나 핵심인지 보여주는 예로, Lisp에서 함수를 호출하고 싶다면 리스트를 만들어서 해요.

흥미롭게도 리스트의 첫 번째 원소(head)는 호출되는 함수를 나타내고, 나머지 원소(tail)는 인자로 전달돼요. 이런 방식을 전위 표기법이라고 하는데(연산자가 피연산자보다 앞에 오는 방식이에요), 처음에는 좀 낯설 수 있지만 사실 아주 유용해요:

  • 연산자를 반복해서 쓰지 않고도 여러 인자에 연산자를 적용할 수 있어요(예: (+ 1 2 3))
  • 연산자 우선순위가 분명해져요. 다른 연산자를 쓰려면 어차피 새 S-표현식을 정의해야 하니까요.

재미있게도 리스트는 소스 코드를 표현하는 데도 쓰여요. 이건 나중에 다시 다룰게요.

일반적으로 대부분의 Lisp은 문법이 아주 최소하고 의미 체계도 비교적 단순해서 배우기 쉽고, 코드를 이해하기도 더 쉬워요. 문법이 최소하다고 해서 덜 강력한 건 아니에요! 이 두 가지(최소한의 문법 + 단순한 의미 체계)가 합쳐지면서 Lisp은 컴파일러와 인터프리터를 작성하기에 이상적인 언어가 돼요. 언젠가 직접 컴파일러를 만들고 싶다면 Lisp를 만드는 것도 좋은 선택이에요!

Lisp의 멋진 특징들

앞서 말했듯이 Lisp은 코드를 표현할 때 내부적으로 같은 데이터 타입과 자료 구조를 써요. 이런 성질을 호모이코니시티(homoiconicity)라고 해요. 다시 말해, 어떤 언어로 작성한 프로그램을 그 언어로 데이터처럼 다룰 수 있고, 따라서 프로그램 자체를 읽기만 해도 그 내부 표현을 알 수 있다면 그 언어는 호모이코닉해요. 이 성질은 흔히 그 언어가 코드를 데이터로 취급한다고 요약해요.

언어들

Scheme

  • 1970년대에 MIT AI Lab의 Guy Steele과 Gerald Sussman이 만들었어요.
  • Carl Hewitt의 액터 모델을 아주 작은 Lisp 인터프리터로 이해해보려는 시도에서 시작됐어요.
  • 이 언어는 일련의 AI 연구 메모를 통해 소개됐는데, 이 메모들은 나중에 통틀어 람다 논문(Lambda Papers)이라고 불리게 됐어요.
  • 렉시컬 스코프(값은 정의된 곳에서만 스코프에 있어요)를 사용한 최초의 Lisp 방언이고, 일급 컨티뉴에이션을 지원한 최초의 언어 중 하나예요.
  • 공식 IEEE 표준과, RnRS(Revised Report on the Algorithmic Language Scheme)라는 사실상 표준이 있어요.
  • 다양한 구현이 있어요: ChezScheme, Guile(둘 다 Exercism에서 지원해요), MIT/GNU Scheme, Racket
  • 문법이 아주 적은 최소한의 언어지만, 그건 의도한 게 아니었어요.
  • 저자들은 복잡한 걸 만들려 했지만 결국 의도했던 것보다 훨씬 단순한 걸 설계하게 됐어요.
  • 제대로 된 꼬리 재귀를 지원해요. 반복을 하는 관용적인 방법이 재귀예요.
  • Scheme은 꼬리 재귀 호출을 최적화해서 스택 공간이나 다른 자원을 소모하지 않아요. 그래서 아무리 큰 데이터에도, 아무리 긴 계산에도 재귀를 쓸 수 있어요.
  • 유리수와 복소수를 포함한 강력한 숫자 데이터 타입
  • 지연 평가를 지원하는데, 프로미스와 비슷해요.
  • 강력한 매크로 시스템.
  • 위생적 매크로는 매크로를 정의할 때 예상치 못한 결과가 생길 가능성을 줄여줘요.

Common Lisp

  • Common Lisp에 대한 작업은 ARPA의 관리자 Bob Engelmore가 커뮤니티 표준 Lisp 방언 하나를 만들자고 제안하면서 1981년에 시작됐어요. 당시 쓰이던 여러 방언이 서로 호환되지 않는 경우가 많아 코드와 지식을 공유할 수 없었거든요.
  • 첫 표준은 1984년에, 최종 표준은 1994년에 발표됐어요(아주 안정적인 명세죠).
  • 표준이다 보니 여러 구현이 있어요. Steel Bank Common Lisp(Exercism의 기본 구현이에요)과 CLisp 같은 것들이죠.
  • 상용 구현도 있어요. Allegro CL, LispWorks 같은 것들이죠. C 프로그램에 내장할 수 있는 ECL(Embeddable Common Lisp)과 자바 가상 머신에서 동작하는 ABCL도 있어요.
  • 표준(ANSI INCITS 226-1994)으로 정의돼 있어서 30년 전에 작성한 코드도 오늘날 잘 동작해요.
  • 풍부하고 확장 가능한 타입 시스템
  • 이미지와 REPL 개발을 염두에 두고 설계돼서 내부를 아주 잘 들여다볼 수 있어요.

Emacs Lisp

  • 텍스트 편집기를 확장할 효율적인 언어를 만들 목적으로 1985년에 개발됐어요.
  • 동적 타입 언어예요.
  • Emacs의 약 80%가 Emacs Lisp로 작성됐어요(성능을 위해 20%는 C로 작성됐죠).
  • 다른 Lisp과는 조금 달라요:
  • 표준화되지 않았고 지금도 천천히 진화하고 있어요.
  • 자동 꼬리 호출 제거가 없어요. named-let 매크로로 지원하는데(while 루프로 변환돼요)
  • 기본은 다이내믹 스코프이고, 새 코드에는 렉시컬 스코프를 권장해요.
  • 편집기 안에 좋은 문서가 있어요.
  • 여러 플랫폼을 지원해요(Emacs가 도는 곳이면 어디서든 돼요).
  • 매일 쓰는 기능의 코드를 읽으면서 언어를 배울 수 있어요(Emacs 코어 + 패키지)
  • cl-lib 패키지를 통해 Common Lisp의 일부를 쓸 수 있어요. Emacs Lisp은 상당히 최소한인 반면 Common Lisp은 훨씬 기능이 풍부하죠. cl-lib 패키지가 CL의 일부를 사용할 수 있게 해줘요.

Racket

  • Matthias Felleisen이 PLT Inc.를 세웠는데, 이 회사는 1995년 1월에 Scheme을 기반으로 한 교육용 프로그래밍 환경을 만들기로 했어요. 처음 이름은 PLT Scheme이었고 나중에 Racket으로 바뀌었죠.
  • 교육용 프로그래밍 환경일 뿐만 아니라 프로그래밍 언어 설계와 구현을 위한 플랫폼으로도 설계됐어요.
  • 현대적인 Lisp이고 Scheme의 후예예요.
  • 논리 프로그래밍을 지원해요!
  • 단순하고 표현력이 좋은 문법으로, 초보자에게도 이상적이고 전문가의 손에서는 강력해요.
  • 다양한 프로그래밍 패러다임을 지원해요: 함수형 프로그래밍, 객체 지향 프로그래밍, 계약에 의한 설계, 논리 프로그래밍, 메타프로그래밍
  • 포괄적인 표준 라이브러리
  • 학습과 탐구를 위해 번거로움을 최소화하도록 설계된 완전한 IDE인 DrRacket이 함께 제공돼요.
  • 배경 정보와 예제가 풍부한 훌륭한 문서

Clojure

  • Rich Hickey가 JVM에서 동작하고 동시성이 뛰어난 현대적인 Lisp을 목표로 개발했어요.
  • Lisp의 방언이지만 다른 Lisp과는 조금 달라요. 암묵적 꼬리 재귀를 지원하지 않고(이게 뭔지 몰라도 걱정 마세요), 리스트 외에도 여러 자료 구조를 갖고 있어요: 맵, 셋, 벡터죠. 이 자료 구조들은 모두 각자의 리터럴 문법을 가져요.
  • 이것들은 모두 불변이지만 O(log32 n) 조회로 성능이 아주 좋아요. 사실상 상수 시간이죠.
  • 멀티메서드와 프로토콜을 통한 런타임 다형성
  • 뛰어난 JVM 상호 운용성
  • Clojure Spec 데이터 명세 시스템(컴파일 타임이 아니라 런타임이에요)으로 데이터 구조를 정의하고, 데이터를 생성하고, 속성 기반 테스트를 하는 등 여러 가지를 할 수 있어요.

활용 사례

Scheme

  • 컴퓨터 과학을 가르치는 교육에서 쓰여요(영향력 있는 '컴퓨터 프로그램의 구조와 해석'도 Scheme을 써요).
  • AI에서 쓰여요. 스크립트 언어로도 쓰이는데, 예를 들어 GIMP(그래픽 편집기), CAD 도구(컴퓨터 지원 설계), 심지어 영화에도 쓰였어요. '파이널 판타지: 더 스피릿 위드인'의 렌더링 엔진 관리 스크립트가 그렇죠.

Common Lisp

  • Common Lisp은 여러 곳에서 쓰여요. 인공 지능과 연구 분야는 물론 상용 애플리케이션에도요. NASA는 딥 스페이스 원 우주선의 자동 항법 소프트웨어를 Common Lisp으로 작성했고, Viaweb도 Common Lisp으로 작성됐는데 나중에 Yahoo가 인수해 Yahoo Store!로 이름을 바꿨어요. Reddit의 첫 버전도 Common Lisp으로 만들어졌죠.

Emacs Lisp

  • Emacs Lisp은, 뭐, Emacs에서 쓰여요!
  • 본질적으로 Emacs는 Emacs Lisp의 인터프리터예요. Emacs Lisp은 Lisp 프로그래밍 언어의 방언인데, 텍스트 편집을 지원하는 확장이 더해졌죠.

Racket

  • 교육에서 쓰여요. Racket은 언어 생성, 단순화, 분석을 지원하는 데 중점을 두고 설계됐으니까요.
  • 연구에서도 쓰여요. 확장 가능한 문법과 의미 체계 덕분에 새로운 언어와 언어 기능을 설계하고 프로토타이핑하는 데 알맞거든요.
  • 게임에서도 쓰여요. 예를 들어 (Doom으로 유명한) John Carmack이 VR용 대화형 스크립팅 환경에 썼고, 개발사 Naughty Dog는 스크립팅에 사용했어요(예: Uncharted에서요). Hacker News는 Arc로 작성됐는데, Arc도 Lisp이고 Racket으로 작성됐죠.

Clojure

  • Clojure는 여러 가지에 쓰여요. 2022년에 Docker가 인수한 Atomist도 Clojure로 구현된 컨테이너 보안 및 자동화 플랫폼이죠.
  • 전 세계에서 Clojure를 가장 많이 쓰는 곳은 신생 은행인 Nubank예요. 몇 년 전에 인수했고 지금은 Clojure 핵심 팀을 고용하고 있죠.
  • 동적이고 상호 작용이 뛰어나서 빠른 프로토타이핑에 많이 쓰여요.

프로그래밍 관점

모든 언어가 함수형, 명령형, 기호적 패러다임을 지원해요. 일부는 객체 지향 프로그래밍도 지원하는데, 특히 Common Lisp이 그렇죠.

Lisp은 대부분 동적 언어지만, Racket은 정적 타입도 지원해요.

그렇다고 해서 모두 인터프리터 방식인 건 아니에요. 컴파일 단계 없이 인터프리터로 실행하는 방식, 바이트코드로 컴파일한 뒤 인터프리터로 실행하는 방식, 기계어로 직접 컴파일하는 방식이 섞여 있거든요.

Scheme

  • 명확하고 단순한 의미 체계와 식을 만드는 몇 안 되는 방법만 있는 최소한의 언어예요.
  • 언어를 배우고 코드를 이해하기 쉽게 해줘요.
  • 그래서 Scheme은 많은 컴퓨터 과학 입문 강의에서도 자주 쓰여요.
  • 일급 컨티뉴에이션.
  • 컨티뉴에이션은 프로그램의 상태를 표현한 것이에요.
  • 컨티뉴에이션은 제어 흐름(예: return 구문)이나 코루틴(멀티태스킹을 가능하게 해줘요)을 모델링하는 데 쓸 수 있어요.

Common Lisp

  • 프로그래밍 가능한 메서드 조합을 갖춘 확장 가능한 객체 지향 시스템이에요(서브클래스와 슈퍼클래스 메서드를 조합하는 방식은 물론, before, after, around 메서드를 통해서도 시스템을 수정하지 않고 확장할 수 있어요).
  • 프로그래밍 가능한 조건 시스템은('예외'의 상위 집합이에요) 조건을 인식하는 일과 조건을 어떻게 처리할지 선택하는 일을 분리해줘요. 조건 시스템은 예외 시스템보다 유연한데, 오류를 알리는 코드와 그것을 처리하는 코드로 둘을 나누는 대신, 조건을 알리는 일, 조건을 처리하는 일, 재시작하는 일 이렇게 세 부분으로 책임을 나누기 때문이에요.
  • 매크로로 언어의 문법을 확장할 수 있어요. 단순히 반복적인 코드를 생성하는 데 그치지 않죠. 이 덕분에 도메인에 맞춰 언어를 만드는 쪽으로 갈 수 있어요. 그 반대가 아니라요.

Emacs Lisp

  • 뛰어난 편집기 지원과 통합
  • 실행 중인 Emacs를 커스터마이즈하는 데 쓸 수 있어요('스스로 뇌 수술을 하는 것' 같은 거죠 :))
  • 배치 모드에서도 쓸 수 있는데, 이 모드에서는 텍스트를 처리하는 편집기 기능(버퍼, 이동 명령 등)을 모두 사용할 수 있어요.

Racket

  • 강력한 매크로 시스템. 스레딩 매크로 같은 문법 설탕이 이 위에 만들어져 있어요. 매크로는 위생적이기도 한데, 이는 간단한 질문에 대한 답이에요. 매크로는 다른 곳에 배치될 코드를 생성해요. 그 코드가 평가될 때 그 안의 식별자 바인딩은 어떻게 정할까요? 위생적 매크로는 매크로를 정의할 때 예상치 못한 결과가 생길 가능성을 줄여줘요.
  • 언어 지향적이에요.
  • Racket은 자체 매크로 위에 자신만의 프로그래밍 언어나 DSL을 작성할 도구를 제공해요.
  • 여러 내장 언어도 있어요. 정적으로 검사되는 타입 애너테이션을 지원하는 typed Racket, DrRacket IDE가 지원하는 datalog(Prolog과 비슷한 언어), HTML이나 PDF 형태의 문서를 만드는 도구인 scribble 같은 것들이죠.
  • REPL은 뭔가 시험해보거나 문서를 찾아보는 용도를 넘어 개발 워크플로의 핵심이에요.

Clojure

  • 강력한 매크로 시스템.
  • 스레딩 매크로 같은 문법 설탕이 이 위에 만들어져 있어요.
  • REPL은 뭔가 시험해보거나 문서를 찾아보는 용도를 넘어 개발 워크플로의 핵심이에요.

무엇을 해볼까요

  • Lisp를 한 번도 써본 적이 없다면 Scheme과 Racket이 좋은 선택이에요. 둘 다 문법이 아주 최소하니까요.
  • 그렇긴 해도 Common Lisp과 Clojure에는 학습 모드가 있으니, Exercism에서 배우기에는 아마 이 둘이 가장 좋을 거예요.
  • 이미 Emacs를 쓰고 있다면 Emacs Lisp가 자연스러운 선택이에요.
  • 마찬가지로 JVM 언어를 쓰고 있다면 Clojure가 자연스러운 선택이에요.
  • Emacs Lisp(Emacs 사용), Clojure(IntelliJ 사용), Racket(DrRacket 사용) 모두 IDE 지원이 훌륭해요.
  • 물론 Common Lisp과 Scheme에도 좋은 IDE가 있어요.
  • 정말 기능이 꽉 찬 Lisp를 원한다면 Common Lisp, Clojure, Racket이 아주 방대해요.
  • 조금 색다른 Lisp를 원한다면 Clojure는 Lisp치고 꽤 독특한 문법을 갖고 있어요.
  • 매크로와 메타프로그래밍에 관심이 있다면 사실 다 좋은 선택이에요! 그런데 새로운 언어를 만들고 싶다면 특히 Racket이 훌륭해요.

물론 시간이 있다면 몇 개는 꼭 함께 써보길 권해요! 그리고 괄호를 두려워하지 마세요! 저도 그랬어서 Lisp 배우는 걸 꽤 오래 미뤘거든요. 그래도 금방 익숙해질 거고, 어쩌면 저처럼 괄호가 좋아질지도 몰라요. 사실 저는 지금 Lisp 계열 언어를 정말 좋아해요. 문법은 최소한이고 의미 체계는 쉬우면서도 표현력이 아주 뛰어나니까요.

2023년 06월 01일 · 유용했나요?