후행이 아닌 구분자는 즐겁지 않다
요약
데이터 구조에서 후행 구분자(Trailing Delimiter)의 유무가 코드 변경 및 유지보수에 미치는 영향을 분석합니다. JSON의 마지막 쉼표 금지 규칙이 가져오는 불편함과 이를 허용하는 Python, Go 등의 사례를 비교합니다.
핵심 포인트
- 후행 쉼표 허용 시 항목 추가/삭제 시 기존 코드 수정 최소화 가능
- JSON은 마지막 쉼표를 허용하지 않아 데이터 수정 시 복잡성 유발
- Haskell, TLA+, Prolog 등 다양한 언어의 구분자 처리 방식 비교
- Python과 Go는 후행 쉼표를 허용하여 코드 변경 편의성 제공
- 데이터 구조에서 항목을 쉼표로 나눌 때
후행 구분자를 허용하면 항목 추가·삭제·재배치가 같은 방식의 텍스트 변경으로 처리됨 - JSON은 마지막 멤버 뒤 쉼표를 금지해 맨 끝에 키를 추가하거나 삭제할 때 기존 줄까지 수정해야 하는
특수 사례가 생김 - Haskell 레코드, TLA+ 변수 선언, Prolog 규칙도 구분자 위치나 종료 기호 때문에 첫 줄·마지막 줄 변경이 서로 다르게 처리됨
- Python과 Go는 후행 쉼표를 허용하지만 선행 쉼표는 허용하지 않으며, Alloy는
선행·후행 쉼표를 모두 허용함 - 후행 구분자는 제어 구문에서는 파싱 모호성을 만들 수 있고, Python의 단일 원소 튜플처럼 데이터 구문에서도 의미 구분에 쓰임
JSON의 마지막 쉼표 문제
- JSON 객체에서 멤버 사이 쉼표는 허용되지만, 마지막 멤버 뒤에 오는 쉼표는 문법상 허용되지 않음
{
"a": 1,
"b": 2,
"c": 3
}
- 같은 객체에서
"c": 3,
처럼 마지막 멤버 뒤에 쉼표를 붙이면 유효하지 않은 JSON이 됨
{
"a": 1,
"b": 2,
"c": 3,
}
- 후행 쉼표가 허용되면
"a"
앞에 "x"
를 추가하고 "c"
뒤에 "y"
를 추가할 때 같은 형태의 줄 추가만 필요함
{
+ "x": 0,
"a": 1,
"b": 2,
"c": 3,
+ "y": 4,
}
- 현재 JSON 문법에서는 마지막 위치에 키를 추가할 때 기존 마지막 줄
"c": 3
에도 쉼표를 붙여야 하므로 변경이 더 복잡해짐
{
+ "x": 0,
"a": 1,
"b": 2,
- "c": 3
+ "c": 3,
+ "y": 4
}
- 요소를 제거할 때도 해당 줄만 지울 수 없고, 마지막 줄에 후행 쉼표가 남지 않는지 확인해야 함
- 객체 값 자체가 여러 줄 배열이나 객체이면 “후행 쉼표 없음”으로 인한 변환이 더 복잡해짐
다른 언어의 비슷한 사례
Haskell 레코드
- Haskell은 레코드 타입에서 쉼표를 각 행의 앞에 두는 “부분 불릿 포인트” 스타일을 사용할 수 있음
data Drone = Drone
{ xPos :: Int
, yPos :: Int
, zPos :: Int
}
-
이 방식은 마지막 행을 바꾸기 쉽게 만들지만, 첫 번째 행을 바꾸기는 더 어렵게 만듦
TLA+
- TLA+에서는 변수 목록과 시퀀스에서 끝 쉼표가 없는 형태는 유효함
VARIABLES a, b, c
vars == <<a, b, c>>
- 같은 구문에서 마지막 항목 뒤에 쉼표를 붙이면 유효하지 않음
VARIABLES a, b, c,
vars == <<a, b, c,>>
- TLA+ 명세를 작성할 때 최상위 변수를 계속 추가하게 되므로 이 제한이 불편해짐
- PlusCal DSL에서는 같은 문제가 없고, 변수 선언을 세미콜론으로 나열할 수 있음
(*--algorithm foo {
variables a; b; c;
Prolog
- Prolog 같은 논리 언어는 후행 구분자를 허용하지 않을 뿐 아니라 별도의 종료 기호를 사용함
foo(A, B, C) :-
A = 1, % comma
B = 2, % comma
C = 3. % period!
- 마지막 마침표를 별도 줄에 두는 방식으로 중괄호처럼 볼 수도 있지만, 표준 구문이 아니며 후행 구분자도 얻지 못함
foo(A, B, C) :-
A = 1,
B = 2,
C = 3
.
더 나은 방식
후행 구분자를 허용하는 언어
- Go는 맵 리터럴에서 마지막 항목 뒤 쉼표를 허용함
valid := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
- Python도 딕셔너리에서 마지막 항목 뒤 쉼표를 허용함
valid = {
"a": 1,
"b": 2,
"c": 3,
}
- Python과 Go의 쉼표는 뒤에 올 수 있지만 앞에는 올 수 없어 완전한 불릿 포인트 스타일은 만들 수 없음
invalid = {
, "a": 1
, "b": 2
, "c": 3
}
선행 구분자와 Alloy
- TLA+는 선행 결합과 선행 논리합 연산을 허용하지만,
(a &&)
처럼 뒤에 붙이는 방식은 허용하지 않음
// Not TLA+ but the same semantics
|| && a == 1
&& b == 2
|| && a == 3
&& b == 4
- Alloy는 선행 쉼표와 후행 쉼표를 모두 허용함
sig Valid {
, a: 1
, b: 2
}
sig AlsoValid {
a: 1,
b: 2,
}
- Alloy는 빈 구분자도 허용해 여러 개의 쉼표만 있는 줄도 유효하게 처리함
sig StillValid {
,, a: 1,,
,,,,,,,,,
,, b: 2,,
}
반론: 파싱 모호성
Prolog의 제어 구분자
- 후행 구분자를 반대하는 논거 중 하나는 파싱이 모호해질 수 있다는 점임
- Prolog에서 마침표로 규칙을 끝내면
foo
와 bar
가 별도 정의임이 분명함
foo(A, B) :-
A = 1,
B = 2.
bar(c).
- 규칙 종료 기호를 쉼표로 바꾸면
bar(c)
가 foo
정의의 일부로 해석될 수도 있음
foo(A, B) :-
A = 1,
B = 2,
bar(c),
- 이 경우
foo
는 bar(c)
도 참일 때만 참인 것으로 해석될 수 있음
Ruby의 메서드 호출
- Ruby에서는 줄바꿈 뒤에도 메서드 체인을 이어 쓸 수 있으며, 아래 코드는 5를 출력함
puts 3.
succ().
succ()
- 메서드 호출 뒤 후행 구분자를 허용하면
quux()
가 최상위 함수인지 foo
의 메서드인지 분명하지 않게 됨
foo.
bar().
baz().
quux()
- Prolog와 Ruby 사례는 데이터 구분자가 아니라 제어 구분자와 관련된 모호성임
데이터 구문에서의 예외: Python 튜플
- Python은 괄호를 표현식 그룹화와 튜플 정의에 모두 사용함
(2+3)
은 표현식 평가로 처리되어 int
가 됨
>>> x = (2+3)
>>> type(x)
<class 'int'>
(2+3,)
은 후행 쉼표 때문에 단일 원소 튜플로 처리됨
>>> x = (2+3,)
>>> type(x)
<class 'tuple'>
- Python의 이 사례는 후행 데이터 구분자가 표현식과 단일 원소 튜플을 구분하는 역할을 함
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기