후행이 아닌 구분자는 즐겁지 않다

1 hour ago 2
  • 데이터 구조에서 항목을 쉼표로 나눌 때 후행 구분자를 허용하면 항목 추가·삭제·재배치가 같은 방식의 텍스트 변경으로 처리됨
  • 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,, }
    • 이런 형태는 일부 사람들에게 “stuttering”이라고 불림

반론: 파싱 모호성

  • 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의 이 사례는 후행 데이터 구분자가 표현식과 단일 원소 튜플을 구분하는 역할을 함
Read Entire Article