1P by GN⁺ 2시간전 | ★ favorite | 댓글 1개
  • zig fmt는 파일에 이미 있는 구문 형태를 반영해 같은 코드도 여러 레이아웃으로 배치할 수 있는 조종 가능한 포매터로 쓰일 수 있음
  • 함수 호출에서는 trailing comma 유무가 결과를 바꾸며, 쉼표가 없으면 한 줄로 합쳐지고 쉼표가 있으면 인자를 줄마다 배치함
  • 실제 흐름은 원하는 코드 배치를 먼저 정하고 쉼표를 몇 개 추가한 뒤 포맷 단축키를 눌러 zig fmt가 나머지를 처리하게 하는 방식임
  • 배열에서는 trailing comma뿐 아니라 첫 번째 줄바꿈 위치도 반영되어, 첫 줄바꿈이 세 번째 항목 뒤에 있으면 항목 3개씩 맞춰 정렬됨
  • ++ 배열 연결을 신중히 쓰면 줄마다 항목 수를 다르게 배치할 수 있고, subprocess에 --keyvalue 쌍을 넘길 때 고정 인자 배열과 옵션 쌍 배열을 연결해 정렬할 수 있음

zig fmt를 조종하는 방식

  • zig fmt는 현재 파일에 이미 있는 구문 형태를 보고 같은 구문도 여러 방식으로 배치할 수 있어 조종 가능한 포매터로 쓰일 수 있음
  • 함수 호출에서 trailing comma 유무가 레이아웃을 바꿈
    f(1, 2,
          3);
    
    // -> zig fmt ->
    
        f(1, 2, 3);
    
    f(1, 2,
          3,);
    
    // -> zig fmt ->
    
        f(
            1,
            2,
            3,
        );
    
  • 실제 사용 흐름은 원하는 코드 배치를 먼저 정하고 ,를 몇 개 추가한 뒤, 포맷 단축키를 눌러 zig fmt가 나머지를 처리하게 하는 방식임
  • 포매터가 레이아웃을 추측하게 하기보다, 사용자가 핵심 선택을 직접 남기는 방식이 더 잘 맞을 수 있음
  • 좋은 포매팅의 90%는 논리 블록 사이의 빈 줄과 적절한 중간 변수 선택에 달려 있으므로, 이런 선택을 제거하기보다 활용하는 편이 낫다는 결론임

배열의 열 맞춤 레이아웃

  • 배열에서는 trailing comma만으로 한 줄에 하나씩 배치되는 것이 아니라, 첫 번째 줄바꿈 위치zig fmt가 반영함
    .{ 1, 2, 3,
          4, 5, 6, 7, 8, 9, 10, 11,  };
    
  • 첫 줄바꿈이 세 번째 항목 뒤에 있으면, 결과도 항목 3개씩 맞춰 정렬됨
    .{
            1,  2,  3,
            4,  5,  6,
            7,  8,  9,
            10, 11,
        };
    
  • ++ 배열 연결을 신중히 쓰면 줄마다 항목 수를 다르게 배치할 수 있음
  • subprocess에 --keyvalue 쌍을 넘길 때는 고정 인자 배열과 옵션 쌍 배열을 연결해 다음처럼 정렬할 수 있음
    try run(&(.{ "aws", "s3", "sync", path, url } ++ .{
        "--include",            "*.html",
        "--include",            "*.xml",
        "--metadata-directive", "REPLACE",
        "--cache-control",      "max-age=0",
    }));
    
Lobste.rs 의견들
  • gofmt에도 비슷한 서식 유도 동작이 있었던 것으로 기억하고, 그런 포매터가 rustfmt보다 더 마음에 듦
    그래도 포매터가 아예 없는 것보다는 어떤 형태의 서식 자동화라도 있는 편이 낫다고 봄

    • “포매터가 없는 것보다 뭐든 있는 게 낫다”는 말은 그냥 넘기기 어렵다
      자동 포매터는 평범함을 강제하고, 서식을 못 쓰는 사람들을 끌어올리지만 잘 쓰는 사람들도 끌어내린다
      협업할 때 다른 사람들이 원하거나 그들의 개인 서식 규율을 신뢰하기 어렵다면 쓰겠지만, 혼자 작업할 때는 절대 쓰지 않음
      내 취향도 있고 포매터의 취향도 있는데, 둘은 화해 불가능하게 다르다
      다만 이 글에서 보여준 내용 자체는 인상적임
      그 문장은 Fiddler on the Roof에서 중매쟁이 Yente가 “나쁜 남편이라도—신이여 막아주소서—남편이 없는 것보다는 낫다!”라고 한 대사를 떠올리게 함
    • 기억하기로 Elm 포매터도 비슷하게 동작했고, 원래 서식을 고려하지 않는 포매터들에 비해 꽤 좋게 느껴졌음
    • 일부 C++ 프로젝트에서 clang-format을 쓰는데 끔찍함
      버전 간 안정성이 너무 낮아서 clang-format 업그레이드가 코드 모든 줄을 건드리는 서식 커밋으로 이어짐
      포매터가 없는 것보다 나은지 정말 확신이 안 듦
    • 예전에는 “어떤 포매터든 없는 것보다 낫다”고 오래 생각했지만, 최근에는 완전히 생각이 바뀜
      자동 포매터는 주로 풀 리퀘스트에서 자전거 헛간 논쟁을 없애는 사람 문제를 해결함
      그런데 이제 에이전트형 개발로 넘어가면서 그 문제는 점점 덜 중요해짐
      지금 여러 프로젝트에서 기계가 대부분의 작업을 하고 있는데, 그렇게 되면 포매터를 돌리지 않는 편이 더 낫다는 쪽으로 느껴짐
  • Python에서 명령줄 인자를 만들 때는 튜플을 리스트에 스플랫하는 방식을 좋아해서, 글의 마지막 예제는 이렇게 쓸 것 같음

    [  
      "aws",  
      "s3",  
      "sync",  
      path,  
      url,  
            *("--include", "*.html"),  
      *("--include", "*.xml"),  
      *("--metadata-directive", "REPLACE"),  
      *("--cache-control", "max-age=0"),  
    ]  
    
  • 마지막으로 봤을 때 zig fmt에는 80열 제한을 100열 제한 대신 쓰도록 설정하는 방법이 없었는데, 아직도 그런가?
    하루에 여러 시간 작업하면 눈이 덜 피로해서 터미널 글꼴 크기를 키워 쓰는데, 80열과 100열 차이는 vim 분할 두 개와 nerd tree를 나란히 둘 수 있느냐를 가름함

    • zig fmt에는 열 제한이 없음
  • 포매터가 전혀 없던 팀에 rigid formatter를 도입한 사람으로서, 가끔은 수동으로 서식에 영향을 줄 수 있는 능력이 그리움
    그런 면에서 Zig가 유연한 건 정말 멋짐

  • 훌륭하다!
    이런 식의 TS/JS 포매터가 있을까?
    maplibre-gl을 쓰는 프로젝트가 있는데 스타일 명세 표현식이 가끔 너무 과하게 서식화돼서 아무것도 안 보임
    지금은 포매터 사용을 멈췄지만, 디버깅하고 복사하고 주석 처리하다 보니 코드가 지저분해지고 있음
    어쩌면 Zig 포매터를 다른 언어도 포매팅하도록 만들 수 있을지도 :)

    • Prettier에도 비슷한 기능이 있지만, 구체적으로는 객체 리터럴에 한정되고 “한 줄에 전부”와 “각 요소를 다른 줄에” 중에서만 고를 수 있음
      예를 들어 한 줄에 네 요소씩 두라고 포매터에 지시할 방법은 없음
      또한 객체 리터럴이 너무 길어지면 입력 텍스트가 어떻게 되어 있든 Prettier가 결국 “각 요소를 다른 줄에” 형식으로 바꿔버림
  • 가벼운 방식의 포매터, 사실상 줄바꿈만 하는 포매터에는 실망한 적이 있어서, 더 엄격한 예시 안에서 이런 유연성을 갖는다는 발상이 부럽고 마음에 듦 :p
    최근 fedi에서 비례폭 글꼴로 Lisp를 작성하고 서식화하는 질문에 답하면서, 의미 있는 공백을 쓰는 s-expression 변형들, 즉 wisp, Readable/Sweet expressions, SRFI 119와 110을 짚었음
    이 문법 계열은 선택적 중위 표기 확장을 활용해 줄바꿈에 대한 제어권을 일부 돌려준다는 관찰도 덧붙였음

  • 흥미로운 설계지만 마음에 드는지는 잘 모르겠음
    내 포매터에서는 다르게 처리함: 포매터가 후행 쉼표를 무시하고 서식을 결정한 뒤, 여러 줄로 나뉘면 항상 후행 쉼표를 추가하고 한 줄이면 항상 후행 쉼표를 제거함
    그래서 “유도”는 못 하지만 f(1, 2, 3)은 후행 쉼표 유무나 토큰 사이 공백의 양과 종류와 관계없이 일관되게 포매팅됨
    어느 정도의 유도는 필요함
    예를 들어 긴 리스트 리터럴 [<expr1>, <expr2>, ..., <expr100>]이 있으면 대부분의 포매터는 각 표현식을 한 줄에 하나씩 두겠지만, 가능한 한 많이 한 줄에 넣고 싶을 수 있음
    이 둘을 후행 쉼표로 결정하는 건 이상하다고 보고, 일반적으로는 선택지가 2개가 아니라 N개일 수 있음
    이런 목적에는 속성이 더 잘 맞는다고 생각함
    예를 들어 이미 있을지도 모르지만, 문장 앞에 #[rustfmt::list_layout(flow)] 같은 것을 붙여 해당 문장 안의 리스트 리터럴 서식에 영향을 주는 식이 가능함
    유도가 너무 많으면 전체 생태계의 코드 서식을 일관되게 만들고 코드 리뷰를 쉽게 하는 포매터의 목적을 해치므로 제한된 경우에만 해야 함
    긴 리스트 리터럴은 정말 필요한 예라고 봄
    내 프로젝트에도 테스트 기대값 리뷰에 서식이 도움이 되는 예가 있는데, 여기가 그렇다
    Dart 포매터에도 다른 “유도” 동작이 하나 떠오름: 긴 리스트 리터럴에서 주석 줄을 추가해 줄들을 그룹화할 수 있음
    예를 들어 [1, 2, 3, ..., 1000]이면 각 요소를 한 줄에 하나씩 두지만, 수동으로 이렇게 그룹화할 수 있음

    [1, 2, 3, 4, 5,  //  
     6, 7, 8, 9, 10, //  
     ...]  
    

    이런 기능을 의도적으로 넣은 것인지, 주석 처리 방식에서 나온 부산물인지는 모르겠음

    • 그 방식이 정확히 rustfmt의 동작이고, 그게 미치게 만듦
      가끔은 줄 길이 제한을 넘기더라도 함수 호출을 나누지 않는 편이 더 읽기 좋은 경우가 있고, 거기에 대한 내 판단을 반영할 수 있으면 좋겠음
      떠오르는 한 가지 사례는 OpenGL임
      보통 하나의 리소스를 수정하거나 사용하면서, 예를 들어 텍스처 초기화처럼 gl.* 호출을 연속으로 많이 하게 되는데 rustfmt는 “줄이 너무 김. 줄을 쪼개야 함”이라는 로봇 같은 목적 말고는 아무 감각 없이 밀어버림
      이 예제는 동작을 보여주기 위한 인위적인 것이고 실제 rustfmt 동작과 완전히 같지는 않음
      줄도 그렇게 길지는 않음
      지금 휴대폰으로 쓰고 있어서 100% 정확한 예제를 만들 도구가 없음
      gl.bind_texture(gl::TEXTURE_2D, tex);  
      gl.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST);  
      gl.tex_parameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST);
      
      // -->
      
      gl.bind_texture(gl::TEXTURE_2D, tex);  
      gl.tex_parameteri(  
          gl::TEXTURE_2D,  
          gl::TEXTURE_MIN_FILTER,  
          gl::NEAREST,  
      );  
      gl.tex_parameteri(  
          gl::TEXTURE_2D,  
          gl::TEXTURE_MAG_FILTER,  
          gl::NEAREST,  
      );  
      
      이런 식으로 이어지는 gl.tex_parameteri 호출을 여러 줄로 쪼개는데, 사실 각 호출은 한 줄에 완전히 펼쳐 두는 편이 더 낫다
      열이 맞춰지면 두 줄의 차이를 훨씬 쉽게 찾을 수 있기 때문임
      쪼갠 버전은 시각적 근접성이 떨어지고 읽기 더 어렵다
      눈으로 두 줄을 쉽게 비교할 수 없게 됨
      또 줄을 문자 수 제한 안에 맞게 포매팅할 수 없을 때 완전히 실패하는 우스운 일도 생김
      컴파일러 코드를 쓰며 문자열 리터럴로 진단 메시지를 만들 때 이런 일이 자주 일어나는데, 메시지가 꽤 길어질 수 있음
      rustfmt는 이를 어떻게 나눌지 몰라서 포기하고 해당 문장 전체를 포매팅하지 않음
      흔히 이런 식임
      match something {  
          // ... match arms above this one ...  
          _ => emit_diagnostic(&mut state, "This is a very long message to try and illustrate the problem. Help: please consult a doctor.")  
      }  
      
      여기서 emit_diagnostic 호출이 표현식일 뿐이라는 이유로 전체 match의 포매팅을 포기하는데, 그냥 어리석다
      내 코드를 최대 100열로 밀어붙이려 하지 않았다면 전부 피할 수 있었음
  • 끝부분의 코멘트를 보고 나처럼 찾아봐야 했던 사람을 위해 적자면, ++배열 연결 연산자
    그래서 배열을 두 개로 나누면 서로 다르게 포매팅할 수 있음