1 คะแนน โดย GN⁺ 2025-01-20 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • help.autocorrect=1 ของ Git จะแก้คำสั่งที่พิมพ์ผิดแล้วรอเพียง 0.1 วินาที ก่อนสั่งรัน จึงแทบจะเหมือนรันทันทีจนผู้ใช้ไม่มีเวลาตรวจสอบหรือยกเลิก
  • พฤติกรรมนี้ไม่ใช่ค่าเริ่มต้นของ Git; ในการตั้งค่าเริ่มต้น Git จะไม่รันคำสั่งที่ไม่รู้จัก แต่จะแสดงเฉพาะ คำสั่งที่คล้ายกัน แล้วจบการทำงาน
  • ในการถกเถียงเมื่อปี 2008 help.autocorrect ถูกเปลี่ยนจากค่าบูลีนเป็น เวลารอในหน่วย decisecond ทำให้ 1 ถูกตีความว่าเป็นการรอ 0.1 วินาที ไม่ใช่ true
  • ปัจจุบัน help.autocorrect รองรับ 0, จำนวนบวก, immediate, prompt, never และหากต้องการให้ยืนยันก่อนรัน prompt เป็นตัวเลือกที่ปลอดภัยกว่า
  • การคาดเดาคำสั่งของ Git อิงจาก Levenshtein distance แบบปรับแก้ และ Scott Chacon ได้เสนอแพตช์ให้จัดการ 1 กับสตริงบูลีนให้เป็นธรรมชาติมากขึ้น

จาก git pushy ไปเป็นการรัน git push

  • เมื่อ @dhh พิมพ์ git pushy Git คาดเดาว่านั่นคือ git push และรอเพียง 0.1 วินาที ก่อนรัน
  • 0.1 วินาทีสั้นเกินไปที่ผู้ใช้จะอ่านคำสั่ง ตัดสินใจว่าถูกต้องหรือไม่ และยกเลิกได้
  • พฤติกรรมนี้ไม่ใช่พฤติกรรมเริ่มต้นของ Git แต่เกิดขึ้นเมื่อเปิดการตั้งค่า help.autocorrect

พฤติกรรมเริ่มต้นและจุดกำเนิดของ help.autocorrect

  • การตอบสนองเริ่มต้นของ Git คือจะไม่รันคำสั่งที่ไม่รู้จัก แต่จะแสดงคำสั่งที่เป็นไปได้ตามความคล้ายของสตริง แล้วจบการทำงาน
    • ตัวอย่าง: git pushy อาจแสดงว่า push เป็นคำสั่งที่คล้ายที่สุด
  • เดิมที Git ตอบคำสั่งที่ไม่รู้จักเพียงว่า “ไม่ใช่คำสั่ง Git” เท่านั้น
  • ในปี 2008 Johannes Schindelin เสนอแพตช์เล็ก ๆ ที่ไล่ดูคำสั่งที่รู้จักเพื่อหาคำสั่งที่คล้ายที่สุด และถ้ามีผู้สมัครที่ใกล้เคียงเพียงรายการเดียวก็ให้รัน
  • Alex Riesen ออกแพตช์ที่ควบคุมสิ่งนี้ด้วยการตั้งค่า help.autocorrect โดยในช่วงแรกการตั้งค่านี้เป็น บูลีน
    • ในการตั้งค่าของ Git ค่าบูลีนสามารถตีความ 1 เป็น true ได้
    • ดังนั้นในบริบทเดิม help.autocorrect=1 จึงอาจดูเหมือนค่าที่เปิดการแก้อัตโนมัติ

เหตุใด 1 จึงกลายเป็น 0.1 วินาที

  • Junio Hamano เสนอให้ help.autocorrect ไม่ใช่บูลีนธรรมดา แต่เป็น จำนวน decisecond ที่ต้องรอก่อนรัน
    • decisecond คือหน่วยละ 0.1 วินาที
    • เจตนาคือให้ผู้ใช้มีโอกาสกด ^C
  • ในแพตช์ที่ถูกรับเข้าไปภายหลัง ค่าการตั้งค่าถูกเปลี่ยนเป็นเวลารอ
  • ผลก็คือ help.autocorrect=1 ไม่ได้หมายถึง “เปิดฟีเจอร์” แต่หมายถึง “รอ 1 decisecond หรือ 0.1 วินาที แล้วจึงรัน”
  • การถกเถียงไม่ได้ระบุไว้ว่าทำไมจึงเลือก decisecond เป็นหน่วยเวลาที่เหมาะสม

ค่าการตั้งค่าที่เลือกได้ในปัจจุบัน

  • ตามเอกสารของ Git help.autocorrect รองรับค่าต่อไปนี้
    • 0: ค่าเริ่มต้น และจะแสดงเฉพาะคำสั่งที่แนะนำ
    • จำนวนบวก: รันคำสั่งที่แนะนำหลังผ่านไปตามจำนวน decisecond ที่กำหนด
    • immediate: รันคำสั่งที่แนะนำทันที
    • prompt: แสดงคำแนะนำและถามยืนยันว่าจะรันหรือไม่
    • never: ไม่รันและไม่แสดงคำสั่งที่แนะนำ
  • หากต้องการให้ยืนยันก่อนรัน prompt เหมาะสมกว่าสำหรับผู้ใช้ส่วนใหญ่
  • ตัวอย่างการตั้งค่าเป็นดังนี้
git config --global help.autocorrect prompt
  • หลังจากนั้น เมื่อป้อนคำสั่งอย่าง git pushy ระบบจะถามด้วย [y/N] ว่าจะรัน push หรือไม่

วิธีที่ Git เลือกคำสั่งที่คล้ายกัน

  • Git ไม่ได้คาดเดาคำสั่งแบบสุ่ม และตั้งแต่จุดหนึ่งเป็นต้นไปจะถือว่าไกลเกินไปจนไม่เสนอผู้สมัคร
  • ตัวอย่างพฤติกรรมมีดังนี้
    • git bass ใกล้กับ rebase มากพอจนแสดงการถามยืนยันการรัน
    • git bassa ถูกตัดสินว่าไม่เหมือน rebase อีกต่อไป จึงไม่รันผู้สมัคร
    • git dm แสดงทั้ง am และ rm เป็นคำสั่งที่คล้ายกัน
    • git dma จับคู่กับ am เพียงรายการเดียว
  • ตรรกะนี้อิงจากอัลกอริทึม Levenshtein distance แบบปรับแก้
    • คำนวณต้นทุนการแก้ไขอักขระที่ใช้ในการเปลี่ยนสตริงหนึ่งให้เป็นอีกสตริงหนึ่ง
    • บางการดำเนินการถูกมองว่ามีต้นทุนแพงกว่าการดำเนินการอื่น
  • Git มี cutoff แบบฮาร์ดโค้ดไว้ หากต้นทุนเมื่อเทียบกับคำสั่งที่รู้จักสูงเกินไป ก็จะถือว่าผู้ใช้พิมพ์ผิดอย่างมากและไม่คาดเดา

แพตช์ที่พยายามเปลี่ยนการตีความ 1

  • เนื่องจาก help.autocorrect=1 ในมุมของมนุษย์แทบจะเหมือน immediate Scott Chacon จึงเขียนแพตช์เล็ก ๆ ให้ตีความ 1 เป็น รันทันที แทนการรอ 0.1 วินาที
  • Junio Hamano ขอว่าอย่าจัดการเฉพาะ "1" เป็นกรณีพิเศษ แต่ให้ตีความ ค่าสตริงบูลีน อย่าง yes, no, true, off ให้ถูกต้องด้วย
  • แพตช์เวอร์ชันที่สองกำลังเดินหน้าไปในทิศทางที่รวมการจัดการสตริงบูลีนเหล่านี้ด้วย
  • หากแพตช์ถูกรวมเข้าไป ความสับสนเกี่ยวกับ help.autocorrect ใน Git เวอร์ชันอนาคตอาจลดลงได้

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

 
GN⁺ 2025-01-20
ความคิดเห็นบน Hacker News
  • ในยุค 70 Hal Finney กำลังพยายามยัด BASIC interpreter สำหรับ Mattel Intellivision ให้ลงใน ROM ขนาด 2KB ซึ่งทุกไบต์มีค่า
    เพื่อแสดงข้อผิดพลาดทางไวยากรณ์ เขาย่อข้อความ error ทั้งหมดเหลือ EH? และทุกวันนี้ก็ยังขำที่เจ้าตัวค่อนข้างภูมิใจกับเรื่องนั้น

    • EH? น่าจะเข้ากันได้ดีในฐานะคำตอบจาก text parser ของเกม interactive fiction สมัยเก่า
      ที่เกี่ยวข้องกันนิดหน่อย BASIC รุ่นเก่าบางสายพันธุ์ก็ใช้ ? แทนคำสั่ง PRINT ด้วย ซึ่งดูเหมือนมีเป้าหมายเพื่อประหยัดเวลาพิมพ์ใน REPL มากกว่าประหยัดหน่วยความจำ
    • สิ้นเปลืองนะ ed ใช้แค่ ? ตัวเดียวกับทุก error เลย ประหยัดกว่า 3 เท่า
    • กรณีแรกสุดที่เคยเห็นการใช้ Eh? เป็นคำตอบของ interpreter คือใน JOSS ของ RAND
      [https://en.wikipedia.org/wiki/JOSS#/media/File:JOSS_Session....](https://en.wikipedia.org/wiki/JOSS#/media/File:JOSS_Session.jpg)
      https://en.wikipedia.org/wiki/JOSS
      หน่วยความจำประมาณ 5KB แต่เมื่อเทียบกับ Intellivision แล้ว เครื่องหนักราว 5,000 ปอนด์
    • เจ๋งดี ไม่เคยรู้ว่า Hal เป็น แฮกเกอร์คอมพิวเตอร์ส่วนบุคคล ในยุคนั้น และพอได้ยินชื่อ Hal ก็มักนึกถึง Bitcoin เสมอ
    • ผมดูแล xordle ซึ่งเป็นเกมดัดแปลงจาก Wordle โดยในบอร์ดเดียวมีปริศนา Wordle สองชุด
      ดังนั้นอาจเดาคำหนึ่งแล้วตัวอักษรทั้ง 5 เป็นสีเขียวทั้งหมด แต่กลับไม่ใช่คำตอบทั้งสองคำก็ได้ เวลานั้นผมให้ขึ้นแค่ Huh? ทางด้านขวา แล้วคนชอบส่วนนั้นกัน
  • ต้นตอของปัญหาคือ ตั้งชื่อ config ได้แย่
    ถ้าตั้งค่าเดิมถูกตั้งชื่อให้ดูเป็นบูลีนอย่าง help.autocorrect_enabled คำขอให้รับค่า integer แล้วตีความเป็นหน่วย 0.1 วินาทีก็คงฟังไม่ขึ้น ต้องมี config อื่นอย่าง help.autocorrect_accept_after_dsec แทน และคำว่า dsec ก็ไม่คุ้นเคยเอามาก ๆ จนคนใช้คงต้องไปค้นดูเอง
    ใน code review ผมมักขอเรื่องแบบนี้เสมอ ถ้ามีโอกาสคลุมเครือ ชื่อตัวแปรต้องมี หน่วย อยู่ด้วย เช่น int timeout ควรเป็น int timeout_msec
    สำหรับชื่อ config เรื่องนี้สำคัญกว่าถึง 100 เท่า เพราะ config เป็นส่วนหนึ่งของ public interface และภายหลังเปลี่ยนไม่ได้

    • เห็นด้วย แต่ยังลังเลอยู่เมื่อเรื่องนี้ถูกผลักเข้าไปใน type system
      ในบริบท C++ เกณฑ์คร่าว ๆ คือ void FooBar(std::chrono::milliseconds timeout); ใช้ได้ เพราะเป็น function signature เวลาอ่านจะเห็น type ไปด้วย
      แต่สำหรับตัวแปร timeout ใช้ไม่ได้ ใน 99% ของกรณี การใช้งานจริงจะเห็นเป็น auto timeout = gl_timeout; หรือ FooBar(timeout); อะไรแบบนี้
      เพราะใน C++ ใช้ auto กันทั่วไป การไล่ดู type ที่แน่ชัดตอนสำคัญจึงค่อนข้างน่ารำคาญ
      เวลาทำงาน C++ ผมใช้ IDE หรือ editor ที่มี language server แต่ไม่มีเวลามานั่งเอาเมาส์ชี้ดู type ทุก 5 วินาที
    • ใช่ และยิ่งทำให้แย่ลงไปอีกด้วยการใช้หน่วยเวลา 0.1 วินาที (decisecond) ซึ่งตลอดชีวิตแทบไม่มีใครใช้ตรง ๆ เลย
      ถ้าเห็นข้อความว่า “จะรันหลังจาก 1ms” ก็คงรีบไปเช็ก config ทันที
    • ทำไปทำมาก็จะได้สภาพแบบ systemd ที่ใช้ได้ทั้ง TimoutSec=60 และ TimeoutSec=1min
      ผมว่าการไม่ใส่หน่วยไว้ในชื่ออาจดีกว่าด้วยซ้ำ แต่ถ้าไม่นับกรณีพิเศษนั้น ก็เห็นด้วยทั้งหมด
    • ผมก็ทำแบบนั้นนะ แต่ก็สลัดความรู้สึกว่ามีกลิ่น Hungarian notation ออกไปยาก
      ทางเลือกที่ดีที่สุดที่พบจนถึงตอนนี้คือให้ค่ารับหน่วยไปด้วย เช่นยอมรับ 5 seconds หรือ 5s และถือว่า 1 เฉย ๆ เป็นค่าที่ผิด
    • ใน code review ผมจะ flag การใช้ int แบบนั้นเอง และแนะนำให้ใช้ value class ที่ส่งหน่วยไปอย่างถูกต้องแทน
      เช่น Second(2) หรือ Millisecond(2000)
      แน่นอนว่าเรื่องนี้ขึ้นกับความสามารถและธรรมเนียมของภาษา
  • xmobar ก็ใช้ หน่วย 0.1 วินาที ในจุดที่คล้ายกันแต่มีปัญหากว่า เป็น config ที่ประกาศว่าจะ refresh แต่ละ section บ่อยแค่ไหน
    ตัวเลขใน config ตัวอย่างเล็กเกินไปจนดูไม่น่าใช่มิลลิวินาที จึงทำให้คนเดาอย่างสมเหตุสมผลว่าเป็นวินาที และสุดท้ายคำสั่งถูกรันบ่อยกว่าที่ตั้งใจ 10 เท่า
    ผมเคยเห็นโหลดพุ่งโดยไม่ตั้งใจจากปัญหานี้หลายครั้ง

  • ดูเป็นการออกแบบที่ค่อนข้างแย่
    แต่ข้อ 1 เป็นผลจากการอ่านบทความผิด และ “ค่าเดิม” นั้นไม่เคยมีอยู่ใน git

    1. การเปลี่ยนที่ตีความค่า config เดิมใหม่อย่างเงียบ ๆ ให้มีความหมายอื่น เช่นเปลี่ยน 1=true เป็น 1=หน่วงเวลายืนยัน 0.100ms เป็นสิ่งที่ควรหลีกเลี่ยงแทบทุกครั้ง ถ้าเคยมีค่าเดิมอยู่ ก็คงอยากลบทิ้ง แต่ก็ควรเปลี่ยนชื่อ config ด้วย
    2. การที่ argument ของ config help.autocorrect เป็นเวลา และวัดด้วยหน่วยที่ไม่เป็นมาตรฐานสำหรับผู้ใช้ส่วนใหญ่ ก็แย่อยู่แล้ว ควรให้เปิดใช้ด้วยบูลีน และให้ปรับเวลายืนยันด้วยค่าทศนิยม
    • สำหรับข้อ 1 ดูเหมือนคุณเข้าใจลำดับเวลาไม่ถูก
      การเปลี่ยนนั้นเกิดขึ้นในปี 2008 ระหว่าง code review ของแพตช์แรกเริ่มที่จะเพิ่ม option นั้นเป็นบูลีน และเกิดก่อนที่จะ commit เข้า main git tree
    • คำว่า “ออกแบบ” ทำให้นึกถึงแผนการในบริบทกว้าง ๆ ที่ตั้งใจไว้
      อันนี้ดูเหมือน กิ่งก้านที่โตขึ้นเองแบบอินทรีย์ มากกว่าการออกแบบ
  • ผมว่านี่เป็นตัวอย่างที่ดีของ feature creep (การเพิ่มฟีเจอร์ลุกลาม)
    มองในแง่ดีก็นำความซับซ้อนที่ไม่จำเป็นเข้ามา มองในแง่ร้ายก็ทำให้คนที่พึ่งพามันใส่ใจกับสิ่งที่ตัวเองทำอยู่น้อยลง

    • git ก็เป็นแบบนั้นพอดี โครงสร้างข้อมูลที่สง่างามถูกทับด้วยชั้นขยะไม่จำเป็นที่สะสมมาตามกาลเวลาหลายชั้น
    • ไม่เข้าใจว่าทำไมถึงอยากอนุญาตให้รันอัตโนมัติ
      กดลูกศรขึ้นแล้วแก้คำผิดมันยากขนาดนั้นเลยเหรอ? การรันสิ่งที่ “ใกล้เคียงพอ” โดยอัตโนมัติเป็นสูตรสำเร็จของการทำสิ่งที่ไม่ได้ตั้งใจ
  • จุดที่ผิดพลาดอยู่ตรงนี้ คือไม่ควรนำค่าตั้งค่าเดิมไปใช้ซ้ำในความหมายอื่น แต่ควรเพิ่ม ค่าตั้งค่าใหม่
    help.autocorrect ควรแยกเป็นเปิด/ปิด ส่วน help.autocorrect.milliseconds ควรเป็นระยะเวลาที่ต้องรอ
    ระบบอื่นก็มีความผิดพลาดคล้ายกัน เช่น innodb_flush_log_at_trx_commit ของ MySQL ถ้าเป็น 0 คือปิดใช้งาน, 1 คือเปิดใช้งาน และ 2 ถูกเพิ่มเข้ามาภายหลังด้วยความหมายพิเศษ

    • ปัญหาที่แท้จริงคือ ภาษาการตั้งค่าที่ไม่มีชนิดข้อมูล ซึ่งพยายามเดาว่า 1 หมายถึงอะไร
      ตอนนี้ 1 ถูกคงไว้เป็นค่าบูลีนจริง ขณะที่จำนวนเต็มอื่นถูกทำให้เป็นหน่วย 0.1 วินาที ยิ่งถลำลึกลงไปอีก
      เป็นตรรกะที่น่าสงสัยแบบเดียวกับ no == false อันฉาวโฉ่ของ YAML
    • ไม่รู้ว่าใช้ตรงไหนถึงจะเหมาะที่สุด แต่ 0.1 วินาที ก็ไม่ใช่ค่าที่ไร้เหตุผล
      ตามบันทึก เวลาตอบสนองแบบสุ่มที่เร็วที่สุดอยู่ราว 1.5ds และบังเอิญว่าเวลาตอบสนองเฉลี่ยของเกมเมอร์ก็ประมาณนั้น แต่การตอบสนองที่ไม่ใช่แบบสุ่มอาจเร็วกว่าได้มาก เช่น กรณีตอบสนองตามจังหวะ
      ถ้าอยากให้เร็วขนาดนั้นก็ทำได้ และความเร็วในการเรียกใช้ก็ควรจะค่อนข้างคงที่ น่าจะอยู่ระดับไม่กี่มิลลิวินาที
  • เมื่อ 1 ปีก่อนเปิดฟีเจอร์แก้อัตโนมัติและตั้งไว้ 3 วินาที แล้วรู้สึกได้สองอย่าง

    1. มันไม่แยกแยะระหว่างการกระทำที่อันตรายกับการกระทำที่ปลอดภัย
    2. มันทำให้ ประวัติของเชลล์ ปนเปื้อนด้วยคำสั่งที่พิมพ์ผิด
      พออ่านบทความนี้แล้ว ก็กลายเป็นแรงผลักดันพอให้ปิดมันหลังจากผ่านไป 1 ปี
    • สร้าง alias ให้คำที่มักพิมพ์ผิดบ่อย ๆ ยังจะดีกว่า
      แน่นอนว่ามันก็ยัง “ปนเปื้อน” ประวัติของเชลล์อยู่ดี
    • สำหรับข้อ 2 คนที่ทำให้ปนเปื้อนจริง ๆ ก็คือตัวเอง
      ถึงจะย้อนประวัติกลับไปแล้วเอาคำสั่งที่ผิดล่าสุดมาใช้ซ้ำทั้งอย่างนั้น เพราะยังไงมันก็ทำงานได้ ก็ไม่ต่างกัน
  • หน่วย 0.1 วินาที เป็นตัวเลือกที่แปลกมากจริง ๆ
    เวลาหน่วงควรกำหนดเป็นมิลลิวินาทีหรือวินาทีมากกว่า และทั้งสองหน่วยก็ใช้กันบ่อยกว่ามากในวงการคอมพิวติ้ง

    • ตอนแรกงงอยู่พักหนึ่ง นึกว่า deciseconds เป็นหน่วยเฉพาะของ git หมายถึง decision-seconds แบบ “วินาทีที่ต้องใช้ในการตัดสินใจ” xD
      อนึ่ง ภาษาอังกฤษไม่ใช่ภาษาแม่ของผม แต่ผมมาจากภูมิภาคศิวิไลซ์ที่ใช้ระบบเมตริก
    • สำหรับเวลาตอบสนองของมนุษย์ก็พอใช้ได้ แต่เป็นหน่วยที่ไม่ค่อยพบ
      ความต่างระหว่าง 0 วินาทีกับ 1 วินาทีนั้นยาวจนสังเกตได้เมื่อรออะไรบางอย่าง แต่ความต่างระหว่าง n มิลลิวินาทีกับ n+1 มิลลิวินาทีนั้นเล็กเกินกว่าจะใช้งานให้เป็นประโยชน์
  • เวลาตอบสนองแตกต่างกันตามชนิดของสิ่งเร้า โดยเสียงจะเร็วกว่า ภาพเล็กน้อย และสัมผัสจะเร็วกว่าเสียงอีกนิด อยู่ราว 90~180ms
    ดังนั้นถ้า git ต่อยคุณสักหมัดแทนการแสดงข้อความผิดพลาด ก็อาจพอมีเวลาตอบสนองได้แบบเฉียดฉิว

    • ถ้าอุปกรณ์ต่อยนั้นต้องให้ความรู้สึกเหมือนโดนจริง ๆ ก็ต้องสร้างแรงเฉื่อยขึ้นมา
      10ms จะพอไหมนะ?
  • อ่านบทความนี้แล้วนึกถึงคำว่า โบราณคดีซอฟต์แวร์ และ “นักโบราณคดีโปรแกรมเมอร์”
    แนวคิดหลังต้องขอบคุณ Vernor Vinge

    • นี่เป็นจุดที่น่าหงุดหงิดจริง ๆ แฮ็กเกอร์พากันยกย่องว่าหนังสือเล่มนั้นเป็นผู้แนะนำแนวคิด “โบราณคดีซอฟต์แวร์” ทำให้ตั้งตารออ่าน 『A Fire Upon The Deep』 มาก
      แต่แนวคิดนั้นถูกบอกใบ้สั้น ๆ ในบทนำเท่านั้น แล้วหลังจากนั้นก็ไม่มีอีกเลย ผลของโบราณคดีเกี่ยวข้องกับพล็อตก็จริง แต่ “นักโบราณคดีซอฟต์แวร์” ไม่ใช่ตัวละครที่มีความหมายต่อเรื่องเล่า
      รู้สึกเหมือนโดนหลอกให้คลิก
    • มักพูดกันว่าอันตรายของโบราณคดีซอฟต์แวร์คือสุดท้ายแล้วคุณจะไปพบ อุจจาระฟอสซิล
    • อดรู้สึกไม่ได้ว่าตอนนี้เหมือนกำลังถูกเรียกว่า “แก่”