10 คะแนน โดย GN⁺ 2025-08-28 | 3 ความคิดเห็น | แชร์ทาง WhatsApp
  • Rust มอบการรับประกันด้านความปลอดภัยที่แข็งแกร่ง ทำให้สามารถทำ รีแฟกเตอร์ กับโค้ดเบสขนาดใหญ่ได้อย่างมั่นใจ จึงช่วยเพิ่มทั้งผลิตภาพและความสามารถในการบำรุงรักษา
  • คอมไพเลอร์สามารถตรวจจับบั๊กที่เกี่ยวข้องกับการจัดตารางแบบอะซิงโครนัสได้ล่วงหน้า และป้องกัน พฤติกรรมที่ไม่กำหนดไว้ เพื่อเสริมความเสถียร
  • ภาษาอย่าง TypeScript มักพบบั๊กแบบอะซิงโครนัสในสภาพแวดล้อมโปรดักชันได้บ่อย เนื่องจากมีระบบชนิดข้อมูลที่ยืดหยุ่นกว่า
  • ระบบชนิดข้อมูล ของ Rust ช่วยบอกผลกระทบจากการเปลี่ยนแปลงโค้ดได้อย่างชัดเจน ทำให้เพิ่มทั้งความเชื่อมั่นและความกล้าทดลองในโปรเจ็กต์ที่ซับซ้อน
  • Zig ต่างจาก Rust ตรงที่มีการตรวจสอบการจัดการข้อผิดพลาดที่หย่อนกว่า จึงอาจพลาดบั๊กจากการพิมพ์ผิดและมีความน่าเชื่อถือน้อยกว่า

สรุปและภูมิหลัง

  • แบ็กเอนด์ของ Lubeno เขียนด้วย Rust 100% และโค้ดเบสได้เติบโตจนถึงจุดที่ยากจะมองภาพรวมทั้งหมดไว้ในหัวได้
    • โดยทั่วไปโปรเจ็กต์ขนาดใหญ่ทำให้ตรวจสอบผลข้างเคียงของการเปลี่ยนแปลงได้ยาก จนนำไปสู่ ผลิตภาพที่ลดลง
  • การรับประกันด้านความปลอดภัย ของ Rust ช่วยบอกผลกระทบของการแก้โค้ดได้อย่างชัดเจน จึงลดความกลัวต่อการรีแฟกเตอร์
    • สิ่งนี้ช่วยยกระดับทั้ง ความสามารถในการบำรุงรักษา และผลิตภาพในระยะยาว
  • บทความนี้เริ่มจากกรณีที่คอมไพเลอร์ Rust ตรวจพบบั๊กแบบอะซิงโครนัส และต่อยอดไปสำรวจข้อได้เปรียบด้านผลิตภาพของ Rust

กรณีตัวอย่างของการรับประกันความปลอดภัยใน Rust

  • สถานการณ์ปัญหา: ห่อสตรักต์ด้วย mutex เพื่อให้เข้าถึงพร้อมกันได้ แล้วทำงานแบบอะซิงโครนัสหลังจากได้ล็อกแล้ว
    let lock = mutex.lock();  
    db.insert_commit(commit).await;  
    
  • การพบปัญหา: rust-analyzer ไม่แสดงข้อผิดพลาด แต่เกิดข้อผิดพลาดตอนคอมไพล์ในไฟล์กำหนดเราเตอร์
    .route("/api/git/post-receive", post(git::post_receive))  
                                         ^^^^^^^^^^^^^^^^^  
    error: future cannot be sent between threads safely  
    
    โฆษณา
  • การวิเคราะห์สาเหตุ:
    • เว็บเฟรมเวิร์กจะสร้างอะซิงก์แทสก์สำหรับแต่ละการเชื่อมต่อ HTTP และ ตัวจัดตารางงาน จะย้ายแทสก์ข้ามเธรด
    • mutex ต้องถูกปลดล็อกในเธรดเดียวกัน หากมีการย้ายเธรดที่จุด .await ก็อาจเกิด พฤติกรรมที่ไม่กำหนดไว้ ได้
    • คอมไพเลอร์ Rust ติดตามอายุการใช้งานของล็อก และตรวจจับความเป็นไปได้ที่จะถูกปลดล็อกในอีกเธรดหนึ่ง
  • วิธีแก้ไข: ปลดล็อกก่อน .await
  • ความสำคัญ: Rust ป้องกันบั๊กแบบอะซิงโครนัสที่ทำซ้ำได้ยากในสภาพแวดล้อมพัฒนาได้ตั้งแต่ตอนคอมไพล์

กรณีเปรียบเทียบกับ TypeScript

  • สถานการณ์ปัญหา: เกิดบั๊กการรีไดเร็กต์แบบอะซิงโครนัสในโค้ด TypeScript
    if (redirect) {  
        window.location.href = redirect;  
    }  
    let content = await response.json();  
    if (content.onboardingDone) {  
        window.location.href = "/dashboard";  
    } else {  
        window.location.href = "/onboarding";  
    }  
    
  • สาเหตุของปัญหา:
    • window.location.href ไม่ได้รีไดเร็กต์ทันที แต่เป็นการตั้งเวลาไว้ ทำให้โค้ดยังคงทำงานต่อ
    • เกิด race condition จนนำไปสู่การรีไดเร็กต์ที่ไม่ตั้งใจ
  • วิธีแก้ไข: เพิ่ม return ในบล็อก if
    if (redirect) {  
        window.location.href = redirect;  
        return;  
    }  
    
  • ข้อจำกัด: TypeScript ไม่มีการติดตาม อายุการใช้งาน หรือกฎการยืมค่า จึงไม่สามารถตรวจพบบั๊กแบบนี้ได้ตั้งแต่ตอนคอมไพล์
    • มักไปพบในโปรดักชันและใช้เวลานานมากในการดีบัก
    โฆษณา

ข้อดีด้านการรีแฟกเตอร์ของ Rust

  • ในงานเว็บดีเวลอปเมนต์ Python, Ruby, JavaScript/Node.js มีผลิตภาพเริ่มต้นสูง แต่เมื่อโค้ดเบสขยายใหญ่ขึ้น การเชื่อมโยงที่หลวมทำให้การเปลี่ยนแปลงทำได้ยาก
    • หลังแก้ไขอาจเกิดข้อผิดพลาดที่ไม่คาดคิด จนลดความตั้งใจที่จะแก้โค้ดต่อ
  • Rust ใช้ ระบบชนิดข้อมูล เพื่อบอกผลกระทบของการเปลี่ยนแปลงอย่างชัดเจน จึงลดความกลัวต่อการ รีแฟกเตอร์
    • ตัวอย่างเช่น การเตือนว่า “การเปลี่ยนแปลงนี้อาจส่งผลต่อส่วนอื่น” ช่วยป้องกันปัญหาได้ล่วงหน้า
  • แม้โค้ดเบสจะเติบโตขึ้น ก็ยัง เพิ่มผลิตภาพ ได้ โดยนำโค้ดเดิมกลับมาใช้ซ้ำและรักษาความปลอดภัยไว้ระหว่างการเปลี่ยนแปลง

เปรียบเทียบกับการทดสอบ

  • การทดสอบมีประโยชน์ต่อการป้องกัน regression ระหว่าง รีแฟกเตอร์ แต่คอมไพเลอร์ไม่ได้บังคับ จึงสามารถละเว้นได้
    • การเขียนเทสต์ต้องตัดสินใจทั้งระดับของนามธรรม พฤติกรรมเทียบกับรายละเอียดการทำงาน และความสามารถในการป้องกันข้อผิดพลาด จึงเป็น ภาระทางความคิด สูง
    โฆษณา
  • Rust ใช้ คอมไพเลอร์ ปิดกั้นความผิดพลาดทั่วไปตั้งแต่ต้น ช่วยลดภาระการตัดสินใจที่ปกติต้องอาศัยเทสต์
    • คุณสมบัติที่ตรวจสอบด้วยระบบชนิดข้อมูลไม่ได้จึงค่อยเสริมด้วยเทสต์

เปรียบเทียบกับ Zig

  • Zig เป็นภาษาระดับระบบที่คล้าย Rust แต่มีความยืดหยุ่นมากกว่าในการจัดการข้อผิดพลาด
    • ตัวอย่าง: โค้ดจัดการข้อผิดพลาด
      const FileError = error{ AccessDenied };  
      fn doSomethingThatFails() FileError!void {  
          return FileError.AccessDenied;  
      }  
      pub fn main() !void {  
          doSomethingThatFails() catch |err| {  
              if (err == error.AccessDenid) {  
                  std.debug.print("Access was denied!\n", .{});  
              }  
          };  
      }  
      
    • การพิมพ์ผิดเป็น AccessDenid ทำให้เกิดบั๊ก แต่คอมไพเลอร์ Zig กลับมองเป็นตัวเลขและคอมไพล์ผ่าน
  • เมื่อใช้คำสั่ง switch จะตรวจจับการพิมพ์ผิดได้ แต่ในคำสั่ง if จะถูกมองข้าม จึงเกิดปัญหาด้านความน่าเชื่อถือ
  • Rust ป้องกันช่องโหว่เชิงการออกแบบเช่นนี้ และตรวจสอบ การพิมพ์ผิด กับข้อผิดพลาดเชิงตรรกะอย่างเข้มงวด

ประเด็นที่ได้

  • Rust ยกระดับทั้งผลิตภาพและความเสถียรของโปรเจ็กต์ขนาดใหญ่ด้วย การรับประกันด้านความปลอดภัย และระบบชนิดข้อมูลที่เข้มงวด
  • แม้เป็นปัญหาซับซ้อนอย่าง บั๊กแบบอะซิงโครนัส ก็ยังตรวจจับได้ตั้งแต่ตอนคอมไพล์ ช่วยลดต้นทุนการบำรุงรักษา
  • กรณีของ TypeScript และ Zig แสดงให้เห็นความเสี่ยงจากการตรวจสอบที่หย่อนกว่า และตอกย้ำคุณค่าของคอมไพเลอร์ที่เข้มงวดของ Rust
  • Rust จึงเป็นเครื่องมือทรงพลังไม่ใช่แค่ในแง่ผลิตภาพช่วงเริ่มต้นของงานเว็บดีเวลอปเมนต์ แต่รวมถึงการจัดการ โค้ดเบส ระยะยาวด้วย

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

 
taptaps 2025-08-30

ทุกครั้งที่เห็นคนพูดกันว่าอันนี้ดีที่สุด เป็นภาษาที่ทรงพลังสุด ๆ!!
ผมก็อดคิดไม่ได้ว่า จริง ๆ แล้วนักพัฒนา Rust มีน้อยกว่าที่คิด เลยพยายามหว่านล้อมให้คนมาใช้ Rust หรือเปล่า??

 
colus001 2025-08-29

มีแค่ผมคนเดียวหรือเปล่าที่พอเห็นบทความแนะนำเกี่ยวกับ Rust แล้วนึกถึงมุกแบบนักชิมที่พูดว่า "ลองเลย! ลองเลย!"?

 
GN⁺ 2025-08-28
ความคิดเห็นบน Hacker News
  • ปีที่แล้วผมพอร์ตไดรเวอร์เครือข่าย virtio-host ที่เขียนด้วย Rust มีทั้งการสลับ backend, กลไก interrupt, และเปลี่ยนจากไลบรารีไปเป็นโปรเซสแบบ standalone เป็นโปรแกรมที่ซับซ้อน ครอบคลุมทั้ง memory mapping, VM interrupt, network socket, และ multithreading ทั้งที่แทบไม่มีประสบการณ์กับ Rust และมีประสบการณ์กับ virtio น้อยมาก แต่พอโปรเจ็กต์คอมไพล์ได้ มันก็ทำงานได้สมบูรณ์เลย ยกเว้นบั๊กเกี่ยวกับ Drop ตัวหนึ่งที่แก้ได้ง่าย ผมคิดว่าเป็นเพราะไลบรารีของ Rust ถูกออกแบบมาให้ใช้งานผิดได้ยาก จึงช่วยไว้มาก

    • ผมพัฒนา Rust มานานแล้ว และส่วนใหญ่ถ้าคอมไพล์ผ่าน โค้ดก็มักจะทำงานได้ดี บางครั้งก็ยังมี deadlock หรือบั๊กเรื่องลำดับการทำงานอยู่บ้าง แต่โดยพื้นฐานแล้ว การคอมไพล์สำเร็จหมายถึงส่วนใหญ่ของโปรเจ็กต์ทำงานได้ดีแล้ว
  • ผมคิดว่า Rust ยอดเยี่ยมมาก แต่ไม่เห็นด้วยกับความเห็นที่ว่า bug การ assign href เป็นความผิดของ TypeScript แก่นของปัญหาคือ ต่อให้ตั้งค่า href แล้ว การย้ายหน้าไม่ได้เกิดขึ้นทันทีแต่ถูกจัดการทีหลัง Rust ก็มีปัญหาแบบเดียวกันได้ ถ้าใน Rust มีฟังก์ชัน set_href และพฤติกรรมนี้ถูกประมวลผลภายหลัง ก็สามารถเขียนโค้ดแบบนี้ได้:

    set_href('/foo')

    if (some_condition) { set_href('/bar') }

    ผมคิดว่าใน Rust คงไม่ออกแบบแบบนี้ การให้ setter เป็นตัวก่อให้เกิด action ไม่ใช่การออกแบบไลบรารีที่ดี และการที่ assign href แล้วไม่ย้ายหน้าทันที ก็แปลกมาก ถ้าเป็น standard library ของ Rust คงไม่มี implementation งี่เง่าแบบนี้ ปัญหาไม่ใช่ Rust vs TypeScript แต่คือความต่างระหว่าง standard library ของ Rust กับ Web Platform API ผมเห็นด้วยว่า Rust คงไม่ให้ประสบการณ์ผู้ใช้แบบนี้

    • ถ้าพูดอย่างเป็นทางการ การออกแบบให้ setter ทำ action ทันทีไม่ใช่สิ่งที่พึงประสงค์ ควรเปลี่ยนชื่อเป็นอะไรอย่าง navigate_to(href) มากกว่า ในสภาพแวดล้อมของเบราว์เซอร์ โค้ด JS ทั้งหมดทำงานในรูป callback และถูกควบคุมโดย event loop ดังนั้นการไม่ทำงานทันทีจึงเป็นเรื่องธรรมชาติด้วย

    • ตัวอย่าง Rust น่าสนใจ แต่จากตัวอย่าง TypeScript อย่างเดียว ยังบอกไม่ได้ว่า TS เหมาะกับโปรเจ็กต์ขนาดใหญ่หรือไม่ ผมไม่ค่อยสบายใจเวลาใช้ Ruby เพราะต้องจับบั๊กตอน runtime บ่อย แต่สุดท้ายถ้าก่อน commit มันทำงานได้ดี และอ่านหรือแก้โค้ดได้ง่าย ผมก็พอใจ ประเด็นการย้ายตำแหน่งเป็นปัญหาของ JavaScript และเป็นสิ่งที่ TS รับทอดมา เกิดจาก JS ที่ยอมให้แก้ไข property ได้ตามใจ แต่หน้าเว็บก็ไม่ได้หายไปทันที ดังนั้นเมื่อรู้พฤติกรรมนี้แล้ว มันก็ถือว่าสมเหตุสมผล

    • ในเชิงเทคนิค ถ้าเป็น Rust ก็อาจส่งสัญญาณความหมายให้ชัดขึ้นได้จากการที่ set_href คืนค่า () หรือ ! แต่ในกรณี redirect แบบมีเงื่อนไข ก็ยังจับการใช้งานผิดได้ยากอยู่ดี

    • สิ่งที่ผมตั้งใจจะสื่อคือ ถ้าเป็น ownership model ของ Rust ก็สามารถออกแบบ API ให้การเรียก window.set_href('/foo') ย้าย ownership ของ window ไป ทำให้เรียกซ้ำสองครั้งไม่ได้ TypeScript ไม่มีแนวคิดเรื่องการติดตาม lifetime แบบนี้อยู่แล้ว จึงทำไม่ได้ และเพราะ JS API มีอยู่ก่อนแล้ว ฝั่ง TypeScript ก็ไม่มีทางนำระบบ ownership เข้ามาได้ ผมแค่อยากชี้ให้เห็นว่าเมื่อฟีเจอร์หลายอย่างของ Rust ทำงานร่วมกัน มันให้การรับประกันที่แข็งแรงกว่า

    • เหตุผลที่คุณบอกว่า Rust ดีกว่า ฟังแล้วเหมือนสุดท้ายคือ “เพราะโปรแกรมเมอร์ Rust เก่งกว่า” ผมไม่คิดว่าโปรแกรมเมอร์ Rust จะใช้ตรรกะวนแบบนี้

  • หลังจาก assign แล้ว โค้ดจะยังทำงานต่อไปถ้าไม่ได้ return ออกก่อนอย่างชัดเจน พูดจริง ๆ นะ ผมไม่เข้าใจว่าทำไมถึงคิดว่าการ assign ค่าเพียงอย่างเดียวจะหยุดการรันของสคริปต์ได้ ตัวอย่าง TS อาจขาดบริบท แต่จะยกมาเป็น "data race" ก็ยังดูแปลก ๆ

    • การ assign ค่าให้ window.location.href มีผลข้างเคียงคือทำให้เบราว์เซอร์นำทางไปยังลิงก์นั้น พฤติกรรมแบบนี้ค่อนข้างเกินคาด ในแง่ที่ว่าการ assign ธรรมดากลับโหลดหน้าใหม่ มันให้ความรู้สึกคล้าย execve จนการคิดว่า JS จะหยุดรันทันทีหลังจากนั้นก็ไม่ใช่เรื่องแปลก เวลาเขียนโปรแกรมไม่ควรพึ่งสมมติฐานแบบนี้ แต่เพราะพฤติกรรมมันแปลกจริง จึงเข้าใจได้ว่าจะสับสน

    • จะคิดแบบนั้นหรือไม่ก็ตาม บั๊กแบบนี้พอมีคนชี้ให้ดูแล้ว วิธีแก้ก็ค่อนข้างชัด ประเด็นหลักที่ผู้เขียนต้องการสื่อคือ บั๊กประเภทนี้ที่ TS จับไม่ได้ ในความเป็นจริงอาจแกะได้ยากและใช้เวลานานมาก

    • exit(), execve() และอย่างอื่นแนวนี้ หยุดการทำงานทันทีจริง ๆ จึงเป็นไปได้ที่คนจะคิดว่า redirect ก็ควรมีพฤติกรรมแบบนั้น

    • การตำหนิคนที่แค่เล่าประสบการณ์ของตัวเองก็ดูแปลก

    • การ assign นี้มีผลข้างเคียงใหญ่คือทำให้หน้าเว็บกำลังจะถูกทิ้งไป จะมองว่าเป็น async action ที่เกิดขึ้นทันทีบ้างก็ไม่เกินเลย ผมเองก็เคยตั้งสมมติฐานแบบนั้น

  • นี่คือเรื่องของนักพัฒนาที่เพิ่งตระหนักว่าระบบ static type มีประโยชน์แค่ไหน อ่านบทความแบบนี้ทีไรก็สนุกทุกครั้ง

    • สิ่งที่ผมอยากแสดงในบล็อกคือ การติดตาม lifetime และระบบ trait ของ Rust สามารถจับปัญหาที่ซับซ้อนกว่าการไม่ตรงกันของ type ได้มาก TypeScript เองก็เป็นภาษา static type แต่ให้การรับประกันได้ไม่เท่า Rust
  • ข้อดีส่วนใหญ่จริง ๆ แล้วไม่ได้มาจากการใช้ static type หรือภาษาที่มีการคอมไพล์หรือ? Java, Go, C++ ก็เหมือนกัน TypeScript มีลูกเล่นตรงที่คอมไพล์ไปเป็น JS และรับปัญหาของ JS มาด้วย แต่ก็ยังใช้งานได้ดี Rust มีระบบ type ที่เข้มงวดกว่า จึงได้การตรวจสอบตอนคอมไพล์เพิ่มขึ้น แต่ก็แลกมากับการเรียนรู้ที่ยากกว่าและอ่านยากกว่าด้วย

    • ก็เห็นด้วยในระดับหนึ่ง แต่ Rust มีมิติอื่นในระบบ type อีกเยอะ เช่น ownership, การเข้าถึงแบบ shared/exclusive, thread safety, sum type และอื่น ๆ ด้วยระบบ ownership/borrow ทำให้รู้ชัดว่าอาร์กิวเมนต์ที่ส่งเข้าไปเป็นแค่มุมมองชั่วคราวหรือเป็นการยกให้ทั้งหมด ซึ่งมีประโยชน์มากในโปรแกรมขนาดใหญ่หรือเวลาต้องใช้ไลบรารีภายนอก ตัวอย่างเช่น slice type ของ Go ไม่ได้ชัดเจนในระดับ runtime ว่า operation ไหนอนุญาตบ้าง และก็ไม่ค่อยมีวิธีที่ชัดในการ borrow แบบอ่านอย่างเดียว Rust สามารถรับประกัน thread safety ได้ในระดับ type system จึงป้องกัน data race ที่ภาษาอื่นอาจหาเจอยากใน runtime ได้ตั้งแต่ตอนคอมไพล์

    • การมองภาษา static type ทั้งหมดเหมือนกัน แปลว่ายังไม่เคยสัมผัสพลังที่แท้จริงของ union(sum) type และ pattern matching พอชินกับ union type แล้ว ก็จะไม่ค่อยพอใจกับภาษา static type แบบดั้งเดิมอีก

    • จุดแข็งใหญ่อีกอย่างคือ traits/impl traits Rust อนุญาตให้เพิ่ม trait ให้กับ type ใด ๆ ภายหลังได้ คล้าย Extension Method ของ C# ภาษาส่วนใหญ่มักกำหนดความสามารถของ type ไว้ตายตัวตั้งแต่ตอนถูกนิยามในไลบรารี แต่ Rust ทำให้ค่อย ๆ สะสมความสามารถเพิ่มลงบน type แบบง่าย ๆ ได้เรื่อย ๆ ความเป็น late-bound แบบนี้ทำให้ระบบ type มีความยืดหยุ่นเชิงพลวัตมากขึ้น ถ้าจะพูดแบบสุดหน่อย พลังพิเศษที่แท้จริงของ Rust ไม่ใช่ borrow checker แต่คือความเปิดกว้างและยืดหยุ่นของระบบ type ไม่จำเป็นต้องออกแบบทุกอย่างให้ครบตั้งแต่ต้น ค่อย ๆ ขยายไปได้

    • ภาษา static type ไม่ได้ให้ผลแบบเดียวกันทั้งหมด Java สุดท้ายก็ยังพึ่ง Object และการ cast ตอน runtime อยู่ดี Go ไม่มี enum ส่วน C++ ถึงจะมีแนวคิด variant เพิ่มมาแล้ว แต่ถ้าจะใช้ให้ปลอดภัยก็ยังต้องจัดการด้วยมือในลักษณะคล้าย try/except ทำให้โครงสร้างใช้งานไม่ค่อยสะดวก

    • ชอบพูดกันว่า Rust เรียนยาก แต่จริง ๆ ถ้าเรียนอย่างจริงจังก็ไม่ได้ยากขนาดนั้น ช่วงเริ่มต้นของการเขียนโปรแกรม มักสำคัญที่จะลองเขียนส่ง ๆ ให้บางอย่างพอทำงานได้ก่อน แต่ Rust ไม่เป็นมิตรกับวิธีนั้น จึงไม่ค่อยเหมาะเป็นภาษาเริ่มต้น อย่างไรก็ตาม ไม่ได้อ่านยากอะไร

  • ความปลอดภัยที่เข้มแข็งของ Rust ทำให้มั่นใจมากขึ้นเวลาต้องไปแตะ codebase ความมั่นใจนี้ทำให้ไม่กลัวที่จะ refactor ส่วนสำคัญ และสุดท้ายก็ช่วยเพิ่มทั้ง productivity และ maintainability ได้มาก แต่ผลลัพธ์แบบนี้จริง ๆ ก็เป็นเหตุผลที่เรามี test ถ้าไม่มี test คอมไพเลอร์ที่เข้มงวดก็ช่วยได้มาก แต่ถ้าเขียน test ดี ๆ ไม่ว่าภาษาไหนก็ควร refactor ได้อย่างมั่นใจ

    • ถ้าส่วนไหนพิสูจน์แบบ static ได้ ก็ควรปล่อยให้คอมไพเลอร์พิสูจน์จะดีกว่า ควรใช้ test เฉพาะในสถานการณ์ที่การรับประกันแบบ static ทำได้ยากเท่านั้น จุดหมายสูงสุดในอุดมคติคือ formal verification แต่ในทางปฏิบัติยากมาก จึงไม่ใช่ข้อสรุปทั่วไป แม้ในเชิงหลักการจะถูกต้องก็ตาม

    • ทั้ง test ที่ดีและการใช้ระบบ type อย่างเหมาะสมต่างก็ช่วยจับบั๊กได้ดี แต่พอพูดถึงการเขียน test ก็ชวนให้นึกถึงการ์ตูน xkcd เรื่อง "Standards" คือแก้ปัญหามาตรฐานด้วยการสร้างมาตรฐานเพิ่ม ในทำนองเดียวกันคือแก้บั๊กด้วยการเขียนโค้ดเพิ่ม ถึงอย่างนั้นการดูแลรักษาระบบ type เป็นหน้าที่ของผู้ออกแบบภาษาอยู่แล้ว จึงไม่ต้องมาบริหารเป็นรายโปรเจ็กต์

    • ทุกครั้งที่ refactor โค้ด ก็ต้อง refactor test ไปพร้อมกัน ทำให้งานเพิ่มเป็นสองเท่า

  • ผมคิดว่าระบบ type ของ Rust หรือ F# เปล่งประกายที่สุดตอน refactor โค้ด คำว่า fearless refactoring นี่เหมาะมาก

    • ข้อเสียคือ Rust ไม่ยอมให้มีโค้ดที่ยังทำไม่เสร็จ ระหว่าง refactor จึงไม่สามารถมี "โค้ดที่ทำงานได้บางส่วน" ต้องทำให้เสร็จทั้งหมดหรือไม่ก็อย่าเริ่มเลย ทำให้ไม่ค่อยสะดวกถ้าจะเขียนโค้ดเชิงทดลอง แต่ความเข้มงวดแบบนี้นี่เองที่พาไปสู่โค้ดที่ดีในท้ายที่สุด
  • ตัวอย่าง Zig นี่ช็อกมาก ดูไม่เสถียรสุด ๆ จนไม่เข้าใจเลยว่าจะมองว่าการออกแบบแบบนี้ดีได้ยังไง

    • ผมว่านี่น่าจะเป็นบั๊ก แต่สำหรับภาษาแบบ creator-centric อย่าง Zig สิ่งสำคัญคือผู้สร้างต้องยอมรับก่อนว่านี่คือบั๊ก จึงจะมีโอกาสถูกแก้ ถ้ามองว่าเป็นพฤติกรรมที่ตั้งใจไว้ ก็อาจเดินหน้ากับการออกแบบนี้ต่อไป

    • ทุกภาษาต่างก็มีการออกแบบที่ไม่นิ่งอยู่บ้าง เช่น Go หรือ Zig ต้องเรียก mutex.unlock() เองเสมอ และจะไม่ปลดล็อกให้อัตโนมัติเมื่อออกจาก scope ตรงกันข้าม Rust มีเรื่องอย่าง operator as ที่ทำให้แปลงชนิดตัวเลขระหว่างกันได้ง่ายมาก และผมเองก็เคยเสียเวลาทั้งวันไปกับการไล่บั๊กจากสิ่งนี้

    • ตอนแรกผมไม่เห็น error นั้น แต่พออ่านคอมเมนต์นี้ถึงได้รู้

    • ผมสงสัยว่า linter น่าจะเตือนเรื่องการอ้างถึง error ที่ไม่มีอยู่ในระบบได้ และชี้แนะให้ใช้ switch ได้ไหม

    • ผมเคยนึกว่า error set ถูกสร้างจาก function signature เสียอีก แปลกดี

  • ผมชอบที่ระบบ static type ที่แข็งแรงและ sound มอบความสามารถหลากหลายแบบนี้ ผมเองก็เคยมีประสบการณ์กับ codebase Haskell ขนาด 1 ล้าน SLOC ที่ทำ refactor ใหญ่ ๆ ได้ง่าย แม้ไม่มีฟีเจอร์หวือหวาอะไร ระบบ type อย่างเดียวก็เพียงพอแล้ว

  • Rust ตรวจจับได้ถูกต้องว่าโค้ดกำลังถือ lock ข้ามขอบเขต await อยู่ แต่การปลด lock ก่อน await จะปลอดภัยจริงหรือไม่ ยังต้องดูบริบทเพิ่ม ผมคิดว่าควรถือ lock ไว้จนกว่าจะสร้าง transaction commit เสร็จ ถ้าปลดก่อน await ก็อาจเกิดปัญหา concurrency ได้ ผมไม่ค่อยรู้เรื่อง Rust async แต่หลัง commit แล้วไม่ควรต้องกันไว้ด้วย join หรือ select หรือ?

    • ถ้าจำเป็นต้องถือ lock ข้าม await ก็ใช้ mutex ที่รองรับ async ได้ เช่นใน crate ของ futures หรือ tokio ที่มี lock แบบนี้อยู่ มักใช้เมื่อจำเป็นต้องถือ lock นาน ๆ หรือค้างข้าม await ซึ่งจะมีต้นทุนสูงกว่า lock ปกติ

    • ถ้าจำเป็นต้องถือ lock ข้ามขอบเขต await ก็สามารถใช้ async-aware mutex ของ Tokio ได้ ดู เอกสาร tokio/sync/struct.Mutex