1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในโครงสร้างข้อมูล เมื่อคั่นรายการด้วยเครื่องหมายจุลภาค การอนุญาตให้มี ตัวคั่นแบบตามท้าย จะทำให้การเพิ่ม·ลบ·จัดเรียงรายการใหม่ ถูกจัดการเป็นการแก้ไขข้อความรูปแบบเดียวกัน
  • 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,
}
  • หากอนุญาตให้มีจุลภาคตามท้าย เวลาจะเพิ่ม "x" หน้า "a" และเพิ่ม "y" หลัง "c" ก็ต้องแค่เพิ่มบรรทัดในรูปแบบเดียวกัน
{
+   "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 สามารถใช้สไตล์ “bulleted list แบบบางส่วน” ในเรคคอร์ดไทป์ โดยวางจุลภาคไว้หน้าของแต่ละบรรทัด
    data Drone = Drone
      { xPos :: Int
      , yPos :: Int
      , zPos :: Int
      }
    
    • วิธีนี้ทำให้แก้บรรทัดสุดท้ายได้ง่ายขึ้น แต่กลับทำให้แก้บรรทัดแรกได้ยากขึ้น
  • TLA+

    • ใน TLA+ รูปแบบที่ไม่มีจุลภาคปิดท้ายในรายการตัวแปรและซีเควนซ์ถือว่าใช้ได้
    VARIABLES a, b, c
    vars ==
    
    • แต่ถ้าใส่จุลภาคหลังรายการสุดท้ายในไวยากรณ์เดียวกันจะถือว่าไม่ถูกต้อง
    VARIABLES a, b, c,
    vars ==
    
    • เวลาสร้างสเปก TLA+ มักต้องเพิ่มตัวแปรระดับบนสุดอยู่เรื่อย ๆ ข้อจำกัดนี้จึงน่ารำคาญ
    • ใน DSL ของ PlusCal ไม่มีปัญหานี้ และสามารถประกาศตัวแปรคั่นด้วยอัฒภาคได้
    (*--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 อนุญาตให้มีจุลภาคหลังรายการสุดท้ายใน map literal
    valid := map[string]int{
            "a": 1,
            "b": 2,
            "c": 3,
        }
    
    • Python ก็อนุญาตให้มีจุลภาคหลังรายการสุดท้ายในดิกชันนารี
    valid = {
      "a": 1,
      "b": 2,
      "c": 3,
    }
    
    • ใน Python และ Go จุลภาคมาอยู่ข้างหลังได้ แต่จะมาอยู่ข้างหน้าไม่ได้ จึงยังทำสไตล์ bulleted list แบบสมบูรณ์ไม่ได้
    invalid = {
        , "a": 1
        , "b": 2
        , "c": 3
    }
    
  • ตัวคั่นนำหน้าและ Alloy

    • TLA+ อนุญาตการเชื่อมแบบนำหน้าและการ OR แบบนำหน้า แต่ไม่อนุญาตรูปแบบที่ไปติดท้ายอย่าง (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 สามารถต่อ method chain ข้ามบรรทัดใหม่ได้ และโค้ดด้านล่างจะพิมพ์ 5
    puts 3.
         succ().
         succ()
    
    • ถ้าอนุญาตตัวคั่นตามท้ายหลังการเรียกเมธอด ก็จะไม่ชัดว่า quux() เป็นฟังก์ชันระดับบนสุด หรือเป็นเมธอดของ foo
    foo.
      bar().
      baz().
    
    quux()
    
    • กรณีของ Prolog และ Ruby เป็นความกำกวมที่เกี่ยวกับตัวคั่นควบคุม ไม่ใช่ตัวคั่นข้อมูล

ข้อยกเว้นในไวยากรณ์ข้อมูล: ทูเพิลของ Python

  • Python ใช้วงเล็บทั้งสำหรับการจัดกลุ่มนิพจน์และการนิยามทูเพิล
  • (2+3) จะถูกประเมินเป็นนิพจน์และได้ค่าเป็น int
>>> x = (2+3)
>>> type(x)

  • (2+3,) จะถูกตีความเป็นทูเพิลสมาชิกเดียวเพราะมีจุลภาคตามท้าย
>>> x = (2+3,)
>>> type(x)

  • กรณีนี้ใน Python แสดงให้เห็นว่าตัวคั่นข้อมูลแบบตามท้ายสามารถทำหน้าที่แยกระหว่างนิพจน์กับทูเพิลสมาชิกเดียวได้

1 ความคิดเห็น

 
GN⁺ 3 시간 전
ความเห็นจาก Lobste.rs
  • ไวยากรณ์ของ JSON ระบุว่าสามารถใส่เครื่องหมายจุลภาคระหว่างสมาชิกสองตัวในอ็อบเจ็กต์ได้ แต่ไม่สามารถใส่ trailing comma หลังสมาชิกได้ ผมไม่คิดว่าจะเรียกสิ่งนี้ว่าเป็น “ความผิดพลาดในการออกแบบ” ได้ เพราะมันไม่ใช่ตัวเลือกตั้งแต่แรก JSON ถูกสร้างขึ้นราวปี 2000~2001 ให้เป็นส่วนย่อยของ ECMAScript 3 และ RFC 4627 แบบ informational ก็ถูกเขียนขึ้นในปี 2006 การที่มันเป็นส่วนย่อยของ JavaScript จึงสามารถทำงานได้ทันทีในเบราว์เซอร์ผ่าน eval คือทั้งเป้าหมายและหัวใจของความสำเร็จของ JSON และ native JSON API ของเบราว์เซอร์ก็เพิ่งถูกเพิ่มเข้ามาในปี 2009 การที่ ES5 ระบุ trailing comma ไว้ในสเปกก็เกิดขึ้นในเดือนธันวาคม 2009 ดังนั้น JSON ที่มี trailing comma จึงไม่มีทางมีอยู่ได้ตั้งแต่แรกเพราะไม่สอดคล้องกับเป้าหมายเดิม

    • ดูเหมือนจะมองว่าเฉพาะการลงมือทำเชิงรุกเท่านั้นที่เป็นความผิดพลาด แต่ผมคิดว่า การไม่ทำอะไรเลย ก็เป็นการเลือกอย่างหนึ่ง ดังนั้นจะเรียกว่าความผิดพลาดก็สมเหตุสมผล
  • นี่เป็นหนึ่งในความน่ารำคาญที่เจอบ่อยที่สุดใน Prolog เวลาทำงานกับเพรดิเคต ถ้าเติม ,true. ไว้ท้ายสุด ก็จะสะดวกมากเพราะเวลาจัดเรียงบรรทัดด้านบนใหม่หรือคอมเมนต์ออก ก็ไม่ต้องคอยกังวลเรื่องจุดปิดท้าย

    • คล้ายกัน ใน SQL ทีมของเราใช้ WHERE clause ในรูปแบบ where true / and ... / and ... โดยเครื่องหมายทับในที่นี้หมายถึงขึ้นบรรทัดใหม่ แบบนี้ทำให้แก้ไขเงื่อนไขไหนก็ได้ง่ายโดยไม่ต้องมีกรณีพิเศษ
  • Zig อนุญาตให้ใช้ trailing comma และยังใช้มันเพื่อควบคุม formatter ที่ตั้งค่าไม่ได้ด้วย .{1, 2, 3,} จะถูกเปลี่ยนเป็นแบบนี้

    .{
       1,
       2,
       3,
    }
    

    และถ้าตัด trailing comma ออกจากลิเทอรัลที่จัดรูปแบบแนวตั้ง ก็จะมีความหมายว่าให้จัดเป็นแนวนอน: .{ 1, 2, 3 } นอกจากนี้ยังใช้ได้กับการนิยาม container type struct { a: u32, b: u32, }, function signature fn foo(a: u32, b: u32,) void {}, function call foo(1, 2,); ในทุกกรณีเหล่านี้ คุณสามารถใช้ trailing comma เพื่อควบคุม auto-format ได้ ผมชอบฟีเจอร์นี้มากจนเพิ่มมันเข้าไปใน HTML language server/auto-formatter ของตัวเองด้วย Before:

    Foo
    
    

    After:

    Foo
    
    

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

  • เห็นด้วย 100% ภาษาใหม่ที่ไม่มี trailing separator สำหรับผมจะโดนหักคะแนนเล็กน้อย มันไม่ถึงขั้นทำให้เกลียดไวยากรณ์ของภาษานั้น แต่เป็นความน่ารำคาญเล็กๆ เหมือนแผลถลอก

    • แล้วกับ function call ล่ะ? foo(1,2,3,4,)? bar(,1,2,3,4)? ถ้า foo() อนุญาตพารามิเตอร์จำนวนแปรผัน งั้นอาร์กิวเมนต์ตัวสุดท้ายคือ nil ไหม? แล้วพารามิเตอร์ตัวแรกของ bar() คือ nil หรือเปล่า?
  • ใน Clojure และ EDN นั้น comma เป็น whitespace โดยปกติจะนิยมใช้คั่นระหว่างคู่คีย์-ค่าของ map literal ที่อยู่บรรทัดเดียวกัน แต่จริงๆ แล้วเป็นทางเลือกทั้งหมด

    {:a 1 :b 2}
    ;=> {:a 1, :b 2}
    {:a,1,,,,,,,,,,:b,2,} ; if you must
    ;=> {:a 1, :b 2}
    
  • ผมคิดว่าสาเหตุใหญ่ที่ทำให้เกิดความตึงเครียดตรงนี้ คือเมื่อมีหลาย clause อยู่ในบรรทัดเดียวกัน ก็จำเป็นต้องมี separator ไม่ทางใดก็ทางหนึ่ง

    function(1, 2, 3, 4)
    

    ในกรณีแบบนี้ การเพิ่มหรือลบอาร์กิวเมนต์โดยให้ต่างกันแค่ระดับบรรทัดมักจะดูขัดๆ เสมอ แม้แต่การคอมเมนต์อาร์กิวเมนต์เพียงตัวเดียวก็ยังต้องใช้ความระวังและความพยายามอยู่บ้าง แลกกับความกระชับของการใส่หลายรายการไว้ในบรรทัดเดียว แต่พอเริ่มวางหนึ่ง clause ต่อหนึ่งบรรทัด ตามอุดมคติแล้วคุณก็จะอยากได้ diff ที่สะอาดขึ้น และวิธีปิดการใช้งานที่ง่ายผ่าน line comment ธรรมดา คำตอบที่ชัดเจนคือปฏิบัติต่อการขึ้นบรรทัดใหม่ให้เป็น separator มาตรฐาน

    function(
      1
      2
      3
    )
    

    หลายภาษาทำแบบนี้กับ ; ถ้าสไตล์ของภาษานั้นสนับสนุนไม่ให้ใส่หลาย statement ในบรรทัดเดียว คุณก็แทบจะไม่เห็น ; เลย ผมไม่ค่อยรู้จักภาษาที่ทำแบบเดียวกันกับ comma แต่ก็เคยอยากลองทำในภาษาเล่นๆ ของตัวเองอยู่เหมือนกัน แน่นอนว่า Lisp เลี่ยงปัญหานี้ไปได้เพราะ subexpression และ subclause ถูกแยกอย่างสมบูรณ์เสมอ จึง ไม่ต้องมี separator เลย

  • นี่เป็นหนึ่งในเหตุผลที่ผมชอบ Lisp หรือพูดให้แม่นคือ S-expression เพราะมีรายละเอียดให้ต้องคิดน้อยลงไปอีกอย่าง