- 데이터 구조에서 항목을 쉼표로 나눌 때 후행 구분자를 허용하면 항목 추가·삭제·재배치가 같은 방식의 텍스트 변경으로 처리됨
- 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의 이 사례는 후행 데이터 구분자가 표현식과 단일 원소 튜플을 구분하는 역할을 함