1 điểm bởi GN⁺ 2 ngày trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi các mục trong cấu trúc dữ liệu được ngăn cách bằng dấu phẩy, việc cho phép dấu phân cách ở cuối giúp thêm, xóa hoặc sắp xếp lại mục được xử lý như cùng một kiểu thay đổi văn bản
  • JSON cấm dấu phẩy sau phần tử cuối cùng, nên khi thêm hoặc xóa khóa ở cuối sẽ xuất hiện một trường hợp đặc biệt buộc phải sửa cả dòng hiện có
  • Bản ghi Haskell, khai báo biến TLA+ và quy tắc Prolog cũng xử lý khác nhau giữa việc sửa dòng đầu và dòng cuối do vị trí dấu phân cách hoặc ký hiệu kết thúc
  • Python và Go cho phép dấu phẩy ở cuối nhưng không cho phép dấu phẩy ở đầu, còn Alloy cho phép cả dấu phẩy đầu và cuối
  • Dấu phân cách ở cuối có thể tạo ra tính mơ hồ khi phân tích cú pháp trong các cấu trúc điều khiển, và cũng được dùng để phân biệt ngữ nghĩa trong cú pháp dữ liệu như tuple một phần tử của Python

Vấn đề dấu phẩy cuối trong JSON

  • Trong đối tượng JSON, dấu phẩy giữa các thành viên được cho phép, nhưng dấu phẩy sau thành viên cuối cùng thì không hợp lệ theo ngữ pháp
{
    "a": 1,
    "b": 2,
    "c": 3
}
  • Trong cùng đối tượng đó, nếu thêm dấu phẩy sau thành viên cuối như "c": 3, thì JSON sẽ không còn hợp lệ
{
    "a": 1,
    "b": 2,
    "c": 3,
}
  • Nếu dấu phẩy cuối được cho phép, thì khi thêm "x" trước "a" và thêm "y" sau "c", chỉ cần cùng một kiểu là thêm dòng
{
+   "x": 0,
    "a": 1,
    "b": 2,
    "c": 3,
+   "y": 4,
}
  • Với ngữ pháp JSON hiện tại, khi thêm khóa ở vị trí cuối, còn phải thêm dấu phẩy vào dòng cuối hiện có "c": 3, nên thay đổi trở nên phức tạp hơn
{
+   "x": 0,
    "a": 1,
    "b": 2,
-   "c": 3
+   "c": 3,
+   "y": 4
}
  • Khi xóa phần tử cũng không thể chỉ xóa đúng dòng đó, mà còn phải kiểm tra xem dòng cuối có còn để lại dấu phẩy cuối hay không
  • Nếu chính giá trị của đối tượng là một mảng hoặc đối tượng nhiều dòng, việc chuyển đổi do “không có dấu phẩy cuối” sẽ càng phức tạp hơn

Những trường hợp tương tự ở ngôn ngữ khác

  • Bản ghi Haskell

    • Haskell có thể dùng kiểu “bullet point một phần” trong kiểu bản ghi, đặt dấu phẩy ở đầu mỗi dòng
    data Drone = Drone
      { xPos :: Int
      , yPos :: Int
      , zPos :: Int
      }
    
    • Cách này giúp việc sửa dòng cuối dễ hơn, nhưng lại làm việc sửa dòng đầu khó hơn
  • TLA+

    • Trong TLA+, dạng danh sách biến và chuỗi không có dấu phẩy cuối là hợp lệ
    VARIABLES a, b, c
    vars ==
    
    • Nhưng nếu thêm dấu phẩy sau mục cuối trong cùng cú pháp thì sẽ không hợp lệ
    VARIABLES a, b, c,
    vars ==
    
    • Khi viết đặc tả TLA+, người ta thường tiếp tục thêm các biến ở mức cao nhất, nên hạn chế này khá bất tiện
    • Trong DSL PlusCal thì không có vấn đề đó, và khai báo biến có thể được liệt kê bằng dấu chấm phẩy
    (*--algorithm foo {
    variables a; b; c;
    
  • Prolog

    • Các ngôn ngữ logic như Prolog không chỉ không cho phép dấu phân cách cuối mà còn dùng thêm một ký hiệu kết thúc riêng
    foo(A, B, C) :-
        A = 1, % comma
        B = 2, % comma
        C = 3. % period!
    
    • Có thể xem cách đặt dấu chấm cuối ở một dòng riêng giống như dấu ngoặc nhọn, nhưng đó không phải cú pháp chuẩn và cũng không mang lại dấu phân cách cuối
    foo(A, B, C) :-
        A = 1,
        B = 2,
        C = 3
    .
    

Cách làm tốt hơn

  • Ngôn ngữ cho phép dấu phân cách cuối

    • Go cho phép dấu phẩy sau mục cuối trong map literal
    valid := map[string]int{
            "a": 1,
            "b": 2,
            "c": 3,
        }
    
    • Python cũng cho phép dấu phẩy sau mục cuối trong dictionary
    valid = {
      "a": 1,
      "b": 2,
      "c": 3,
    }
    
    • Trong Python và Go, dấu phẩy có thể đứng sau nhưng không thể đứng trước, nên không thể tạo kiểu bullet point hoàn chỉnh
    invalid = {
        , "a": 1
        , "b": 2
        , "c": 3
    }
    
  • Dấu phân cách đầu và Alloy

    • TLA+ cho phép phép hội đứng đầu và phép tuyển đứng đầu, nhưng không cho phép dạng gắn ở phía sau như (a &&)
    // Not TLA+ but the same semantics
    || && a == 1
       && b == 2
    
    || && a == 3
       && b == 4
    
    • Alloy cho phép cả dấu phẩy đầu lẫn dấu phẩy cuối
    sig Valid {
        , a: 1
        , b: 2
    }
    
    sig AlsoValid {
        a: 1,
        b: 2,
    }
    
    • Alloy còn cho phép cả dấu phân cách rỗng, nên những dòng chỉ có nhiều dấu phẩy vẫn được coi là hợp lệ
    sig StillValid {
        ,, a: 1,,
        ,,,,,,,,,
        ,, b: 2,,
    }
    
    • Kiểu này được một số người gọi là “stuttering

Phản biện: tính mơ hồ khi phân tích cú pháp

  • Dấu phân cách điều khiển trong Prolog

    • Một trong những lập luận phản đối dấu phân cách cuối là chúng có thể làm nảy sinh tính mơ hồ khi phân tích cú pháp
    • Trong Prolog, khi kết thúc quy tắc bằng dấu chấm, ta thấy rõ foobar là hai định nghĩa riêng biệt
    foo(A, B) :-
        A = 1,
        B = 2.
    
    bar(c).
    
    • Nếu đổi ký hiệu kết thúc quy tắc thành dấu phẩy, bar(c) có thể bị hiểu là một phần của định nghĩa foo
    foo(A, B) :-
        A = 1,
        B = 2,
    
    bar(c),
    
    • Trong trường hợp đó, foo có thể được diễn giải là chỉ đúng khi bar(c) cũng đúng
  • Lời gọi phương thức trong Ruby

    • Trong Ruby, có thể tiếp tục chuỗi gọi phương thức sau khi xuống dòng, và đoạn mã dưới đây sẽ in ra 5
    puts 3.
         succ().
         succ()
    
    • Nếu cho phép dấu phân cách cuối sau lời gọi phương thức, sẽ không còn rõ quux() là hàm cấp cao nhất hay là phương thức của foo
    foo.
      bar().
      baz().
    
    quux()
    
    • Các ví dụ Prolog và Ruby liên quan đến tính mơ hồ của dấu phân cách điều khiển chứ không phải dấu phân cách dữ liệu

Ngoại lệ trong cú pháp dữ liệu: tuple Python

  • Python dùng dấu ngoặc cho cả việc nhóm biểu thức và định nghĩa tuple
  • (2+3) được xử lý như đánh giá biểu thức và cho ra int
>>> x = (2+3)
>>> type(x)

  • (2+3,) được xử lý là tuple một phần tử nhờ dấu phẩy cuối
>>> x = (2+3,)
>>> type(x)

  • Trường hợp này trong Python cho thấy dấu phân cách dữ liệu ở cuối đóng vai trò phân biệt giữa biểu thức và tuple một phần tử

1 bình luận

 
Ý kiến trên Lobste.rs
  • Cú pháp JSON quy định có thể đặt dấu phẩy giữa hai thành viên của một đối tượng, nhưng không được đặt dấu phẩy cuối sau thành viên cuối cùng. Tôi không nghĩ có thể gọi đây là một “lỗi thiết kế”. Vì khi đó đó không phải là một lựa chọn JSON được tạo ra vào khoảng năm 2000–2001 như một tập con của ECMAScript 3, và RFC 4627 mang tính thông tin được viết vào năm 2006. Việc là một tập con của JavaScript để có thể chạy ngay bằng eval trong trình duyệt chính là mục tiêu và là yếu tố cốt lõi dẫn đến thành công của JSON, còn API JSON native của trình duyệt thì mãi đến năm 2009 mới được thêm vào ES5 cũng chỉ chính thức đặc tả dấu phẩy cuối vào tháng 12 năm 2009, nên JSON có dấu phẩy cuối vốn dĩ không thể tồn tại nếu muốn đúng với mục tiêu ban đầu

    • Có vẻ bạn chỉ xem những hành động chủ động mới là sai lầm, nhưng không làm gì cả cũng là một lựa chọn, vì vậy gọi đó là sai lầm cũng là điều hợp lý
  • Đây là một trong những điều bất tiện tôi gặp thường xuyên nhất với Prolog. Khi làm việc với predicate, nếu gắn ,true. ở cuối thì khi sắp xếp lại các dòng phía trên hoặc comment chúng đi, sẽ tiện hơn vì không phải để ý đến dấu chấm cuối cùng

    • Tương tự, trong SQL đội của chúng tôi dùng mệnh đề WHERE theo dạng where true / and ... / and .... Ở đây dấu gạch chéo nghĩa là xuống dòng Làm vậy thì có thể chỉnh sửa bất kỳ điều kiện nào dễ dàng mà không cần xử lý đặc biệt
  • Zig cho phép dấu phẩy cuối, và có thể dùng nó để điều khiển formatter không thể cấu hình .{1, 2, 3,} sẽ được đổi thành:

    .{
       1,
       2,
       3,
    }
    

    Và nếu bỏ dấu phẩy cuối trong một literal được định dạng theo chiều dọc, thì điều đó mang nghĩa yêu cầu căn theo chiều ngang: .{ 1, 2, 3 } Điều này cũng hoạt động với: định nghĩa kiểu container struct { a: u32, b: u32, }, chữ ký hàm fn foo(a: u32, b: u32,) void {}, lời gọi hàm foo(1, 2,); Trong tất cả các trường hợp này, có thể dùng dấu phẩy cuối để điều khiển auto-format Tôi thích tính năng này đến mức đã thêm nó vào language server/trình auto-format HTML của mình Before:

    Foo
    
    

    After:

    Foo
    
    

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

  • Đồng ý 100%. Cá nhân tôi sẽ trừ điểm nhẹ với bất kỳ ngôn ngữ mới nào không có dấu phân cách cuối. Không đến mức ghét ngữ pháp của ngôn ngữ đó, nhưng là một kiểu khó chịu nhỏ như vết xước

    • Cả trong lời gọi hàm nữa sao? foo(1,2,3,4,)? bar(,1,2,3,4)? Nếu foo() cho phép số lượng tham số biến đổi thì đối số cuối là nil à? Tham số đầu tiên của bar()nil à?
  • Trong Clojure và EDN, dấu phẩy là khoảng trắng. Thường nó được dùng theo thói quen giữa các cặp khóa-giá trị trong map literal trên cùng một dòng, nhưng hoàn toàn là tùy chọn

    {:a 1 :b 2}
    ;=> {:a 1, :b 2}
    {:a,1,,,,,,,,,,:b,2,} ; if you must
    ;=> {:a 1, :b 2}
    
  • Tôi nghĩ một lý do lớn khiến ở đây nảy sinh căng thẳng là vì khi có nhiều mệnh đề trên cùng một dòng, bằng cách nào đó vẫn cần dấu phân cách

    function(1, 2, 3, 4)
    

    Trong trường hợp này, nếu thêm hoặc xóa đối số theo kiểu chênh lệch từng dòng thì trông lúc nào cũng hơi kỳ. Ngay cả khi chỉ comment một đối số cũng cần một chút chú ý và công sức. Đổi lại là bạn chấp nhận sự gọn gàng của việc đặt nhiều mục trên một dòng Nhưng một khi bắt đầu để mỗi dòng chỉ một mệnh đề, thì lý tưởng nhất là sẽ muốn các chênh lệch sạch hơn và một cách vô hiệu hóa dễ dàng chỉ bằng comment dòng đơn giản Câu trả lời hiển nhiên là coi xuống dòng như dấu phân cách tiêu chuẩn

    function(
      1
      2
      3
    )
    

    Nhiều ngôn ngữ làm điều này với ;. Nếu phong cách khuyến khích không đặt nhiều câu lệnh trên một dòng thì bạn gần như sẽ không thấy ;. Tôi không biết ngôn ngữ nào làm điều tương tự với dấu phẩy, nhưng đã từng muốn thử trong một ngôn ngữ hobby Tất nhiên, Lisp thì tránh được vấn đề này vì biểu thức con và mệnh đề con luôn được tách biệt hoàn toàn, nên thậm chí không cần dấu phân cách

  • Đây là một trong những lý do tôi thích Lisp, chính xác hơn là S-expression. Bớt đi một chi tiết phải suy nghĩ