개요
S-expression(S-표현식)은 “Lisp의 괄호 문법"으로 흔히 소개되지만, 실제로는 데이터의 외부 표현(external representation) 이자, 많은 Lisp 계열 언어에서 리더(reader) 가 입력 텍스트를 데이터 구조로 바꾸는 문법 규칙 전체를 가리킵니다. S-expression을 제대로 이해하면 다음 질문들이 한 번에 정리됩니다.
- 왜
(f x y)형태가 함수 호출로 해석되는가 - 왜
(a . b)같은 점표기(dotted pair)가 존재하는가 - 왜
'x가 단순한 “문법 설탕"이 아니라 코드=데이터의 관문인가 - Scheme·Common Lisp·Clojure에서 “같아 보이지만 다른” reader 문법 차이는 무엇인가
이 포스트에서는 고전적 정의 → 핵심 문법 치트시트 → 언어별 reader 차이 → read vs eval 구분 → 실전 팁 순으로 S-expression 문법을 체계적으로 정리합니다.
S-expression의 고전적 정의(원형)
John McCarthy의 1960년 논문 Recursive Functions of Symbolic Expressions and Their Computation by Machine 에서 S-expression은 다음 두 가지로 정의됩니다.
- Atom(원자): 원자 심볼은 S-expression이다.
- Ordered pair(순서쌍): \(e_1\)과 \(e_2\)가 S-expression이면 \((e_1 \cdot e_2)\)도 S-expression이다.
오늘날 우리가 쓰는 리스트 표기는, 이 순서쌍(cons cell)을 연쇄로 이어 붙인 약식 표기에 해당합니다. 즉 리스트는 “순서쌍의 특수한 나열"로 정의됩니다.
Read와 Eval 파이프라인
S-expression을 문법으로 볼 때 헷갈림의 대부분은 읽기(read) 와 평가(eval) 를 섞어서 생각하는 데서 옵니다. 다음 구분을 명확히 두는 것이 중요합니다.
- read: 소스 텍스트 → 데이터 구조(리스트·심볼·숫자·문자열 등)
- eval: 데이터 구조 → 실행/값(언어의 평가 규칙에 따름)
즉 (f x y)는 read 단계에서는 그저 “세 원소를 가진 리스트"이고, eval 단계에서 비로소 “첫 원소를 연산자로 보고 나머지를 인자로 평가"하는 규칙이 적용됩니다(언어별 예외와 확장 존재).
flowchart LR
subgraph readPhase["Read 단계"]
A["소스 텍스트"]
B["리더"]
C["데이터 구조"]
A --> B
B --> C
end
subgraph evalPhase["Eval 단계"]
C --> D["평가기"]
D --> E["값 또는 부수효과"]
end
B -->|"텍스트 → datum"| C
D -->|"데이터 → 실행"| E
핵심 문법 치트시트
아래는 “무엇이 S-expression(데이터)으로 읽히는가“에 초점을 둔 치트시트입니다. 평가/실행 규칙은 언어마다 더 다양하므로, 여기서는 리더가 만들어 내는 데이터 형태만 정리합니다.
1) Atom(원자)
- 심볼:
foo,+,map,my-namespace/foo(언어별 허용 문자·네임스페이스 규칙 상이) - 숫자:
42,3.14 - 문자열:
"hello"
2) List(리스트)
괄호로 둘러싼 형태는 대부분 “리스트"로 읽힙니다.
| |
3) Dotted pair / Improper list(점표기, 비정상 리스트)
S-expression을 “순서쌍"으로 본다면 가장 원형에 가까운 표기는 점표기입니다.
| |
리스트는 cons의 연쇄이며, proper list는 마지막 cdr이 nil(또는 해당 언어의 빈 리스트)인 형태입니다. 언어마다 빈 리스트·nil 취급이 조금씩 다릅니다.
| |
점표기는 데이터 구조를 정확히 표현할 때(예: (key . value) 같은 연관쌍) 특히 중요합니다. “약식이 어떻게 풀리는지"를 cons로 보면 직관이 생깁니다.
| |
리스트와 dotted pair의 구조 차이를 도식하면 다음과 같습니다.
flowchart LR
subgraph properList["Proper list (a b c)"]
P1["cons a"]
P2["cons b"]
P3["cons c"]
P4["nil"]
P1 --> P2
P2 --> P3
P3 --> P4
end
subgraph improperList["Improper list (a b . c)"]
I1["cons a"]
I2["cons b"]
I3["atom c"]
I1 --> I2
I2 --> I3
end
4) Quote: 'x가 의미하는 것
대부분의 Lisp 계열에서 'exp는 아래의 약식입니다.
| |
즉 리더 단계에서 이미 '는 리더 매크로(reader macro) 로 동작해, 입력을 (quote ...) 형태의 데이터로 바꿉니다. “평가하지 말고 그대로 두라"는 지시가 데이터 구조로 들어가는 셈입니다.
5) Quasiquote / Backquote + Unquote(+ Splicing)
quote가 “그대로 두기"라면, quasiquote(backquote)는 “템플릿에 일부만 평가 값을 끼워 넣기"에 가깝습니다.
Scheme·Common Lisp 계열 (표기: `, ,, ,@) 예:
| |
Clojure는 같은 개념을 다른 기호로 씁니다 (표기: `, ~, ~@).
| |
언어별 reader 문법 차이
S-expression 자체는 개념이지만, 실제로는 각 언어의 reader를 만나게 됩니다. “비슷해 보이지만 다르다"는 지점을 요약합니다.
Scheme (R5RS): datum / read syntax
Scheme 표준 문서는 “입력 문자열을 datum으로 읽는 규칙"을 문법으로 정의합니다. 리스트·dotted pair·quote·quasiquote가 읽기 단계에서 어떻게 해석되는지 확인하기 좋습니다.
- dotted pair:
(a . b) - quote:
'x - quasiquote:
`(...)+,/,@
예:
| |
Common Lisp: reader macro characters
Common Lisp의 reader는 표준 macro character가 풍부합니다. 특히 .(점표기), '(quote), `(backquote), ,/,@(unquote/splicing)이 “입력 → 데이터” 변환을 규정합니다. backquote는 구현에 따라 내부적으로 append·list·cons 조합으로 변환되며, 리더가 특정한 데이터(코드)를 만들어 준다는 점이 중요합니다. 또한 readtable을 통해 “어떤 문자가 어떤 방식으로 읽히는지"를 커스터마이즈할 수 있어, DSL·언어 제작에서 reader 단계가 특히 강력합니다.
Clojure: S-expression + 추가 리터럴
Clojure는 리스트 기반 표현을 쓰지만, reader 레벨에서 다음 리터럴을 기본 제공합니다.
- vector:
[1 2 3] - map:
{:a 1 :b 2} - keyword:
:user/id
syntax-quote(백틱)는 단순 quote보다 강해, 네임스페이스 자동 수식(qualification) 등 매크로 작성에 유리한 규칙이 있습니다. 즉 “S-expression을 읽는다"는 동일한 출발점 위에, 언어가 리더 규칙을 더 얹은 사례입니다.
| |
같은 표기, 다른 의미: 빈 리스트와 nil/false
reader 문법이 비슷해도, “빈 리스트·거짓·없음"의 의미론은 언어마다 다릅니다.
| 언어 | 빈 리스트 / 거짓 |
|---|---|
| Common Lisp | NIL은 빈 리스트이면서 거짓 |
| Scheme | '()(빈 리스트)와 #f(거짓) 분리 |
| Clojure | nil과 false 분리, 빈 컬렉션은 truthy |
실전 팁: 자주 하는 실수 6가지
- 리더 vs 평가 혼동:
(1 2 3)은 “리스트로 읽히지만”, 평가 단계에서는 (언어에 따라) 함수 호출 규칙으로 해석될 수 있다. - Dotted list는 구조를 바꾼다:
(a b . c)는(a b c)와 완전히 다른 자료구조다. - 공백은 토큰 경계:
foo-bar는 심볼 하나,foo - bar는 토큰 세 개다. - Quote 없으면 심볼이 ‘값’으로 해석된다:
(a b)에서a가 함수/연산자로 평가되는 계열이 많다. - Quasiquote 중첩 깊이: quasiquote 안에서 unquote가 언제 해석되는지는 중첩 규칙이 있다(매크로 작성 시 특히 주의).
- 언어별 literal 확장: Clojure의 벡터·맵·키워드처럼, “괄호만이 전부"가 아닌 언어가 많다.
요약: read vs eval이 문법 이해의 열쇠
S-expression을 “문법"으로 볼 때 가장 중요한 구분은 read와 eval입니다.
- read: 텍스트 → 데이터 구조 (리스트·심볼·숫자·문자열·…)
- eval: 데이터 구조 → 실행/값 (언어의 평가 규칙에 따름)
(f x y)는 read 단계에서는 그저 “리스트"이고, eval 단계에서 비로소 “첫 원소를 연산자로 보고 나머지를 인자로 평가"하는 규칙이 적용됩니다. 이 구분을 명확히 두면 dotted pair·quote·quasiquote·언어별 차이까지 한 번에 정리할 수 있습니다.
참고 자료
- Recursive Functions of Symbolic Expressions and Their Computation by Machine (Part I) — John McCarthy (1960), S-expression 원형 정의.
- Revised(5) Scheme — Syntax — Scheme R5RS datum/read syntax.
- Common Lisp HyperSpec — Section 2.4.6 Backquote — CL reader macro, backquote 규격.
- Clojure — The Reader — Clojure reader forms, syntax-quote, 리터럴.
![Featured image of post [Programming] S-expression 문법: dotted pair부터 quasiquote까지](/post/2026-02-04-s-expression-syntax/wordcloud_hu_22e78f6e25ff2653.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[C++] cout 소수점 자릿수·정밀도 제어 (precision, fixed)](/post/2022-03-29-cpp-cout-precision/wordcloud_hu_b376048fefd128.webp)
![[Philosophy] 시간의 본질: 계산적 관점에서 바라본 시간과 관찰자](/post/2024-10-17-on-the-nature-of-time/wordcloud_hu_de7748521f005f14.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)