1P by GN⁺ | ★ favorite | 댓글 1개
  • 데이터 구조에서 항목을 쉼표로 나눌 때 후행 구분자를 허용하면 항목 추가·삭제·재배치가 같은 방식의 텍스트 변경으로 처리됨
  • 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에서 마침표로 규칙을 끝내면 foobar가 별도 정의임이 분명함
    foo(A, B) :-
        A = 1,
        B = 2.
    
    bar(c).
    
    • 규칙 종료 기호를 쉼표로 바꾸면 bar(c)foo 정의의 일부로 해석될 수도 있음
    foo(A, B) :-
        A = 1,
        B = 2,
    
    bar(c),
    
    • 이 경우 foobar(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의 이 사례는 후행 데이터 구분자가 표현식과 단일 원소 튜플을 구분하는 역할을 함

댓글과 토론

Lobste.rs 의견들
  • JSON 문법은 객체의 두 멤버 사이에 쉼표를 둘 수 있지만 멤버 뒤에 후행 쉼표를 둘 수 없다고 되어 있음. 이걸 “설계 실수”라고 부를 수는 없다고 봄. 선택지가 아니었기 때문임
    JSON은 2000~2001년쯤 ECMAScript 3의 부분집합으로 만들어졌고, 정보성 RFC 4627은 2006년에 작성됨. JavaScript의 부분집합이라 브라우저에서 eval로 바로 동작한다는 점이 JSON의 목적이자 성공의 핵심이었고, 브라우저의 네이티브 JSON API는 2009년에야 추가됨
    ES5에서 후행 쉼표가 명세화된 것도 2009년 12월이라, 후행 쉼표가 있는 JSON은 애초에 목적에 맞지 않아 존재할 수 없었음

    • 적극적인 행동만 실수라고 보는 듯하지만, 아무것도 하지 않는 것도 선택이며 따라서 실수라고 부르는 것도 타당하다고 봄
  • Prolog에서 가장 자주 겪는 불편 중 하나가 이거임. 술어를 작업할 때 마지막에 ,true.를 붙여 두면, 위쪽 줄을 재정렬하거나 주석 처리할 때 마지막 마침표를 신경 쓰지 않아도 돼서 편함

    • 비슷하게 SQL에서도 우리 팀은 where true / and ... / and ... 형태로 WHERE 절을 씀. 여기서 슬래시는 줄바꿈을 뜻함
      이렇게 하면 어떤 조건이든 특별 취급 없이 쉽게 편집할 수 있음
  • Zig는 후행 쉼표를 허용하고, 이걸 설정 불가능한 포매터를 제어하는 데 쓸 수 있음
    .{1, 2, 3,}는 다음처럼 바뀜

    .{  
       1,  
       2,  
       3,  
    }  
    

    그리고 세로로 포맷된 리터럴에서 후행 쉼표를 빼면 가로 정렬을 요청하는 의미가 됨: .{ 1, 2, 3 }
    다음에도 동작함: 컨테이너 타입 정의 struct { a: u32, b: u32, }, 함수 시그니처 fn foo(a: u32, b: u32,) void {}, 함수 호출 foo(1, 2,);
    이런 모든 경우에 후행 쉼표로 자동 포맷을 제어할 수 있음
    이 기능이 너무 마음에 들어서 내 HTML 언어 서버/자동 포매터에도 추가했음
    Before:

    <div foo="bar" style="very-long-string" >Foo</div>  
    

    After:

    <div foo="bar"  
         style="very-long-string"  
    >Foo</div>  
    

    https://github.com/kristoff-it/superhtml

  • 100% 동의함. 후행 구분자가 없는 새 언어는 개인적으로 약간 감점함. 그 언어의 문법을 싫어할 정도는 아니지만 작은 상처 같은 불편함임

    • 함수 호출에서도 그런가? foo(1,2,3,4,)? bar(,1,2,3,4)? foo()가 가변 개수 매개변수를 허용한다면 마지막 인자는 nil인가? bar()의 첫 번째 매개변수는 nil인가?
  • Clojure와 EDN에서는 쉼표가 공백임. 보통 같은 줄의 맵 리터럴에서 키-값 쌍 사이에 관례적으로 쓰지만 완전히 선택 사항임

    {:a 1 :b 2}  
    ;=> {:a 1, :b 2}  
    {:a,1,,,,,,,,,,:b,2,} ; if you must  
    ;=> {:a 1, :b 2}  
    
  • 여기서 긴장이 생기는 큰 이유는 같은 줄에 여러 절이 있을 때 어떤 식으로든 구분자가 필요하기 때문이라고 봄

    function(1, 2, 3, 4)  
    

    이런 경우 줄 단위 차이로 인자를 추가하거나 제거하면 늘 어색해 보임. 인자 하나만 주석 처리할 때도 약간의 주의와 노력이 필요함. 대신 한 줄에 여러 항목을 넣는 간결함을 받아들이는 것임
    하지만 한 줄에 하나의 절만 두기 시작하면, 이상적으로는 더 깔끔한 차이와 단순한 줄 주석으로 쉽게 비활성화하는 방법을 원하게 됨
    명백한 답은 줄바꿈을 표준 구분자로 다루는 것임

    function(  
      1  
      2  
      3  
    )  
    

    많은 언어가 ;에 대해 이렇게 함. 한 줄에 여러 문장을 넣지 않도록 권장하는 스타일이면 ;를 거의 보지 않게 됨. 쉼표에도 똑같이 하는 언어는 잘 모르지만, 취미 언어에서 시도해 보고 싶었던 적은 있음
    물론 Lisp는 하위 표현식과 하위 절이 항상 완전히 구분되어 있으므로 구분자가 아예 없어 이 문제를 우회함

  • Lisp, 정확히는 S-표현식을 좋아하는 이유 중 하나가 이거임. 생각할 세부사항이 하나 줄어듦