ชุมชนโปรแกรมเมอร์ Rust กำลังหารือกันอย่างคึกคักเกี่ยวกับข้อจำกัดที่มีมายาวนานใน derive macros ของภาษา โดยเฉพาะอย่างยิ่งเรื่อง trait Clone
การถกเถียงมุ่งเน้นไปที่ว่าแนวทางปัจจุบันของ Rust นั้นมีข้อบกพร่องพื้นฐานหรือเป็นเพียงการแลกเปลี่ยนที่สมเหตุสมผลที่นักพัฒนาได้เรียนรู้ที่จะปรับตัวให้เข้ากับมัน
ปัญหาหลักของพฤติกรรม Derive ปัจจุบัน
เมื่อคุณใช้ #[derive(Clone)]
ใน Rust คอมไพเลอร์จะต้องการให้พารามิเตอร์ generic ทั้งหมดใช้งาน Clone
ได้ แม้ว่าพารามิเตอร์เหล่านั้นอาจไม่จำเป็นต้องถูก clone จริงๆ ก็ตาม สิ่งนี้สร้างข้อจำกัดที่ไม่จำเป็นซึ่งป้องกันไม่ให้โค้ดคอมไพล์ได้ในกรณีที่มันควรจะทำงานได้ตามตรรกะ ตัวอย่างเช่น wrapper รอบ Arc<T>
ไม่สามารถ derive Clone
ได้ แม้ว่า Arc<T>
เองจะสามารถ clone ได้โดยไม่ขึ้นกับว่า T
จะใช้งาน Clone
ได้หรือไม่
ชุมชนได้ระบุว่านี่เป็นปัญหาที่แพร่หลายซึ่งไม่เพียงส่งผลกระทบต่อ Clone
เท่านั้น แต่ยังรวมถึง derivable traits อื่นๆ เช่น PartialEq
, Eq
, และ Debug
ด้วย สิ่งนี้บังคับให้นักพัฒนาต้องเขียน manual implementations สำหรับสิ่งที่ควรจะเป็น automatic derivations ที่ตรงไปตรงมา
พฤติกรรม Derive ปัจจุบันเทียบกับ Derive ที่สมบูรณ์แบบ
ด้าน | Derive ปัจจุบัน | Derive ที่สมบูรณ์แบบ |
---|---|---|
ข้อจำกัดของ Generic | ต้องการให้พารามิเตอร์ generic ทั้งหมดใช้งาน trait ได้ | ต้องการเฉพาะประเภทของ field ที่ใช้งาน trait ได้ |
ตัวอย่างข้อจำกัด | T: Clone สำหรับ struct Wrapper<T>(Arc<T>) |
Arc<T>: Clone สำหรับ struct Wrapper<T>(Arc<T>) |
ผลกระทบต่อ Semver | เสถียร - ข้อจำกัดไม่เปลี่ยนแปลงเมื่อมีการแก้ไข field | อาจทำให้เกิดปัญหา - ข้อจำกัดเปลี่ยนแปลงเมื่อมีการเปลี่ยนแปลง field ส่วนตัว |
ความซับซ้อนในการใช้งาน | ง่าย คาดเดาได้ | ต้องการการสนับสนุนการจับคู่ trait แบบวงจร |
ความท้าทายทางเทคนิคเบื้องหลัง
การหารือเผยให้เห็นว่านี่ไม่ใช่เพียงการมองข้ามที่สามารถแก้ไขได้อย่างรวดเร็ว การใช้งาน perfect derive จะต้องการการเปลี่ยนแปลงที่สำคัญในระบบ trait matching ของ Rust คอมไพเลอร์จะต้องจัดการกับ cyclical trait matching ซึ่งปัจจุบันทำงานได้เฉพาะกับ auto traits เช่น Send
เท่านั้น
นอกจากนี้ยังมีความกังวลเรื่อง semver compatibility ด้วย กับ perfect derive การเปลี่ยนแปลงประเภทของ private field อาจทำให้โค้ดของผู้ใช้งานเสียหายในลักษณะที่ไม่คาดคิด หาก library เปลี่ยนจากการเก็บ Arc<T>
เป็นการเก็บ T
โดยตรง trait bounds จะเปลี่ยนแปลงโดยอัตโนมัติ ซึ่งอาจทำให้เกิดการล้มเหลวในการคอมไพล์สำหรับผู้ใช้งาน library นั้น
มุมมองของชุมชนเกี่ยวกับการแก้ปัญหา
ชุมชน Rust แบ่งออกเป็นสองฝ่ายในเรื่องวิธีการจัดการกับข้อจำกัดนี้ นักพัฒนาบางคนโต้แย้งให้คงระบบปัจจุบันไว้ให้เรียบง่ายและคาดเดาได้ โดยมองว่าการใช้งาน manual trait implementations เป็นต้นทุนที่ยอมรับได้เพื่อความชัดเจน คนอื่นๆ ผลักดันให้มีการแก้ปัญหาที่ซับซ้อนมากขึ้นซึ่งจะวิเคราะห์ความต้องการของ field จริงๆ แทนที่จะเป็นข้อจำกัดของพารามิเตอร์ generic แบบครอบคลุม
ซอฟต์แวร์ที่สง่างามวัดได้จากจำนวนบรรทัดของโค้ดที่คุณไม่จำเป็นต้องเขียน
สมาชิกชุมชนหลายคนได้ชี้ไปที่ crates ที่มีอยู่แล้วเช่น derive_more
, derivative
, และ educe
เป็นการแก้ปัญหาเฉพาะหน้าที่ใช้ได้จริง third-party solutions เหล่านี้ใช้งานพฤติกรรม derive ที่ฉลาดกว่าแล้ว แม้ว่าจะเพิ่ม dependency overhead ให้กับโปรเจกต์ก็ตาม
มีทางเลือกอื่นที่สามารถใช้ได้
- crate derive_more: ฟังก์ชันการทำงานของ derive ที่ขยายออกไปพร้อมกับ bounds ที่ยืดหยุ่นมากขึ้น
- crate derivative: ให้การควบคุมพฤติกรรมของ derive อย่างละเอียดผ่าน attributes
- crate educe: มอบการใช้งาน derive ที่สามารถปรับแต่งได้
- การเขียนโค้ดด้วยตนเอง: เขียนการใช้งาน trait ด้วยมือเมื่อ derive ไม่เพียงพอ
- proc macros แบบกำหนดเอง: สร้าง derive macros เฉพาะโปรเจกต์ที่มีพฤติกรรมตามที่ต้องการ
เส้นทางข้างหน้า
ในขณะที่บางคนแนะนำว่าสิ่งนี้สามารถจัดการได้ผ่าน RFC และการเปลี่ยนแปลงภาษาในที่สุด ไทม์ไลน์สำหรับการปรับเปลี่ยนดังกล่าวน่าจะใช้เวลาหลายปีเนื่องจากลักษณะที่ทำลายความเข้ากันได้ของการเปลี่ยนแปลง การหารือของชุมชนเน้นย้ำถึงความตึงเครียดระหว่างความเสถียรของภาษาและความสะดวกสบายของนักพัฒนา
ในตอนนี้ นักพัฒนายังคงพึ่งพา manual implementations หรือ third-party crates เมื่อพวกเขาพบกับข้อจำกัดเหล่านี้ การถกเถียงเน้นย้ำว่าแม้แต่ฟีเจอร์ของภาษาที่ดูเหมือนง่ายๆ ก็สามารถเกี่ยวข้องกับการแลกเปลี่ยนการออกแบบที่ซับซ้อนซึ่งส่งผลกระทบทั้งต่อการใช้งานปัจจุบันและการพัฒนาภาษาในระยะยาว
อ้างอิง: Why not?