- Ante เป็นการออกแบบภาษาระบบที่ต้องการใช้ทั้งความยืดหยุ่นของ reference counting และความปลอดภัยของ borrow checking พร้อมหลีกเลี่ยง runtime panic แบบ Rust หรือ overhead จากการตรวจสอบ exclusive access ขณะรันไทม์แบบ Swift
- กลไกหลักคือ shape-stability และ temporary uniq conversion ซึ่งทำให้สร้าง mutable borrow ให้กับฟิลด์ของค่าที่ถูกนับอ้างอิงได้อย่างปลอดภัย และจัดการค่าภายใน union เป็น
uniqเฉพาะในขอบเขตที่จำกัด Rc<RefCell<T>>ของ Rust หากใช้ผิดอาจเกิด panic ตอนรันไทม์ได้ ส่วน borrowing system ของ Swift มีการตรวจสอบ exclusive access ตอนรันไทม์ แต่ Ante พยายามจัดการบางกรณีด้วยกฎตอนคอมไพล์- ยังเป็นการออกแบบแบบ work-in-progress ที่นำไปใช้จริงแล้วเพียงบางส่วน และเนื่องจากต้องวิเคราะห์ชนิดแบบ recursive เพื่อตัดสินว่าสามารถไปถึงอ็อบเจกต์ใดอ็อบเจกต์หนึ่งได้หรือไม่ การเพิ่มฟิลด์จึงอาจกลายเป็น breaking API change ได้
- แนวทางนี้ทำให้สมมติฐานที่ว่า shared mutable borrowing เป็นไปไม่ได้เสมออ่อนลง และกำลังขยายพื้นที่ข้อยกเว้นในการออกแบบความปลอดภัยหน่วยความจำร่วมกับเทคนิคอย่าง Vale, group borrowing และ Rust GhostCell
สิ่งที่ Ante ต้องการผสานเข้าด้วยกัน
- Ante เป็นภาษาสำหรับ system programming ที่มุ่งเป็น Rust ที่เรียบง่ายกว่า พร้อม ความปลอดภัยของหน่วยความจำ และ ความปลอดภัยของเธรด
- โมเดลพื้นฐานคือ ownership เดี่ยวและ borrow checking โดยค่าจะถูกวาง inline บน stack หรือภายใน struct/array ที่บรรจุมันอยู่
- เมื่อต้องการให้ความเรียบง่ายมาก่อน สามารถเลือกใช้ reference counting ได้โดยใส่คีย์เวิร์ด
sharedให้กับ type - ฟังก์ชัน
balanceของ red-black tree ที่ใช้shared type Colorและshared type RbTree tสั้นพอ ๆ กับตัวอย่าง Python และเล็กกว่าตัวอย่าง C++/Rust - ประเด็นหลักคือวิธีจัดการเมื่อ borrow ข้อมูลที่ถูกนับอ้างอิงแบบ mutable โดยไม่มีความเสี่ยง panic จาก
borrow_mut()แบบ Rust หรือการตรวจสอบ exclusive access ตอนรันไทม์แบบ Swift - Ante ยังอยู่ในสถานะ work-in-progress โดยบางส่วนทำงานแล้ว บางส่วนยังเป็นทฤษฎี และการออกแบบยังเปลี่ยนแปลงอยู่
- ดูความคืบหน้าได้ที่ เว็บไซต์ Ante และ Discord
shape-stability และ mutable reference หลายตัว
- shape-stability ของ Ante คือแนวคิดที่ว่า “reference ไปยังเป้าหมายที่มี stable shape จะยังใช้ได้เสมอ ไม่ว่าจะมีการเปลี่ยนแปลงใดเกิดขึ้นที่อื่น”
- ด้วยแนวคิดนี้ จึงสามารถมี mutable borrow reference หลายตัวไปยัง struct เดียวกันได้พร้อมกัน
- ในตัวอย่าง
heal (healer: mut Entity) (target: mut Entity)สามารถเรียกself_healเพื่อให้ตัวเองรักษาตัวเองได้โดยส่งEntityเดียวกันเป็นอาร์กิวเมนต์ทั้งสองตัว- แม้
healerและtargetจะชี้ไปยังEntityเดียวกัน โค้ดนี้ก็ไม่สามารถทำลายEntityได้ ดังนั้น reference ทั้งสองจึงยังคงใช้ได้
- แม้
- mutable reference ไปยังตัว struct เอง ฟิลด์ของมัน และฟิลด์ย่อยของฟิลด์ ก็อาจได้รับอนุญาตพร้อมกันได้
- แม้ใช้
ship: mut Spaceshipและengine_alias: mut Engine = ship.engineพร้อมกัน ก็ถือว่าระหว่างฟังก์ชันทำงานshipและengineข้างในจะไม่ถูกทำลาย
- แม้ใช้
- Rust และ Swift ไม่อนุญาตรูปแบบที่มี
&mutreference หลายตัวชี้ไปยังข้อมูลเดียวกันพร้อมกัน
mutable borrowing ฟิลด์ของค่าที่ถูก reference counted
- ใน Ante หากเติม
sharedหน้าคำนิยาม type ชนิดนั้นจะถูก reference counted โดยอัตโนมัติ - ในตัวอย่าง
shared mut type Spaceshipฟังก์ชันlaunchถือSpaceshipที่เทียบได้กับRcไว้ และส่งmut ship.engineให้set_fuel - เนื่องจาก
launchยังคงรักษาอ็อบเจกต์ที่บรรจุอยู่คือSpaceshipไว้ จึงตัดสินได้ว่าฟิลด์engineของมันยังมีชีวิตอยู่ด้วย - กฎทั่วไปคือสามารถสร้าง
mutborrow reference ให้กับฟิลด์ของ type แบบshared mutได้เสมอ- แต่ไม่ได้หมายความว่าจะสร้าง mutable borrow ให้กับทุกสิ่งที่อยู่ลึกเข้าไปในฟิลด์นั้นได้เสมอไป ต้องมีกฎแยกต่างหาก
- ตัวอย่างหลังจากนี้ใช้รูปแบบที่ชัดเจนกว่าอย่าง
Rc Spaceshipแทน syntax sugarshared mut type Spaceshipshared mut type Spaceshipจะกลายเป็นtype Spaceshipและvar ship: Spaceshipจะกลายเป็นvar ship: Rc Spaceship
จุดที่ union สร้างปัญหาด้านความปลอดภัย
- union เก็บเนื้อหาแบบ inline ได้ จึงลดการไล่ pointer และ cache miss ซึ่งเป็นผลดีต่อ ความเร็ว
- เมื่อ
union Engineของ C อยู่ภายในstruct Spaceshipทั้งStringTheoryEngineและImpulseEngineจะอยู่ในหน่วยความจำของSpaceship - ตรงข้ามกับแนวทางแบบ Java ที่ใช้อินเทอร์เฟซและ pointer
- เมื่อ
- ปัญหาคือการรองรับ union อย่างปลอดภัยในภาษาที่ memory-safe ทำได้ยาก
- ในตัวอย่างที่
Engineเป็นStringTheoryEngine(str: String)หรือImpulseEngine(fuel: I32)หากshipและother_shipชี้ไปยังSpaceshipเดียวกัน อาจเกิด segmentation fault ได้- หลังจากจับ reference ภายใน string ด้วย
match uniq ship.engine - แล้วเปลี่ยน engine เดียวกันเป็น variant อื่นด้วย
other_ship.engine := ImpulseEngine 0x42 - จากนั้นแก้ไข
strเดิม ก็จะเกิดปัญหาใช้งานสิ่งที่อยู่ข้างในหลังจาก container ถูกทำลายไปแล้ว
- หลังจากจับ reference ภายใน string ด้วย
- ดังนั้น Ante ต้องห้ามไม่ให้สร้าง mutable borrow reference ไปยัง variant ใด variant หนึ่ง เมื่อ mutable borrow reference ชี้ไปยัง union
- กฎนี้ตรงข้ามกับกฎของ struct
- หากมี
mutreference ไปยัง struct สามารถสร้างmutreference ไปยังฟิลด์ได้ - หากมี
mutreference ไปยัง union จะสร้างmutreference ไปยังสิ่งที่อยู่ภายใน variant ไม่ได้
- หากมี
uniq และ temporary uniq conversion
uniqหมายถึง exclusive mutable reference หรือ reference แบบ mutable ที่เป็นเอกสิทธิ์- หากตัวแปรหนึ่งถือ
uniq Spaceshipอยู่ นั่นคือ reference เดียวที่ใช้งานได้ไปยังSpaceshipนั้น- เป็นแนวคิดคล้าย
&mut Spaceshipของ Rust
- เป็นแนวคิดคล้าย
- เพื่อจัดการภายใน union อย่างปลอดภัย Ante ใช้ temporary uniq conversion
- กฎหลักคือ หากไม่ใช้ reference อื่นที่อาจเป็น alias ในขอบเขตหนึ่ง ๆ ก็สามารถได้
uniqreference ชั่วคราว- ในช่วง
match uniq ship.engineจะเข้าถึงship.engineเสมือนเป็นuniq - ระหว่างช่วงนี้ compiler จะไม่อนุญาตให้ใช้ตัวแปรเดิมอื่นที่อาจบรรจุ
Spaceshipทางอ้อมได้
- ในช่วง
- Rust ป้องกันการมีอยู่ของ
uniqเองด้วยเหตุผลว่า “อาจมี reference อื่นอยู่ที่ไหนสักแห่ง” แต่ Ante อนุญาตuniqภายใต้เงื่อนไขว่า ไม่ใช้ reference เหล่านั้นในขอบเขตนั้น - ในกรณีนี้
uniq Spaceshipไม่ได้เป็น reference เดียวทั่วทั้งโปรแกรมจริง ๆ แต่เป็น reference เดียวที่ใช้งานได้ภายในขอบเขตนั้น- ให้ความรู้สึกคล้าย pointer แบบ
restrictใน C
- ให้ความรู้สึกคล้าย pointer แบบ
การเข้าถึงที่อนุญาตและถูกปฏิเสธ
- ภายในขอบเขต
match uniq ship.engineหากเข้าถึงother_ship: Rc Spaceshipควรเกิด compile error- เพราะ
other_ship.engineอาจเป็น alias กับship.engine - และระหว่างที่ใช้
ship.engineการเปลี่ยนother_ship.engineอาจทำให้เกิด drop ได้
- เพราะ
- struct อื่นที่มี
Rc Spaceshipเป็นฟิลด์ เช่นHasAShipก็ถูกปฏิเสธด้วยเหตุผลเดียวกันother.ship.engineก็อาจไปถึงSpaceshipเดียวกันทางอ้อมได้
- ในทางกลับกัน สามารถใช้จำนวนเต็มอย่าง
new_fuel: I32ได้- เพราะ
I32ไม่สามารถบรรจุ reference ไปยังSpaceshipได้
- เพราะ
- หาก
Spaceshipเองมีฟิลด์อย่างfollow_ship: Rc Spaceshipก็จะถูกปฏิเสธ- ในกรณีนั้น
uniq Spaceshipก็จะกลับไปถึงตัวเองได้ผ่านเส้นทางภายในตัวมันเอง ดังนั้นโดยทั่วไปจึงไม่สามารถแปลงmut -> uniqกับ recursive type ได้
- ในกรณีนั้น
ข้อจำกัดในการเรียกและคืนค่าฟังก์ชัน
mut -> uniqconversion อาจเกิดขึ้นในการเรียกฟังก์ชันด้วย- เมื่อ
foo (var ship: Rc Spaceship) (new_res: Resonator)เรียกmaybe_use_resonator ship new_resที่จุดเรียกshipจะถูกแปลงเป็นuniq Spaceship- compiler เพียงต้องตรวจสอบว่าอาร์กิวเมนต์อื่นอาจมี reference ไปยัง
Spaceshipหรือไม่ Resonatorในตัวอย่างไม่มี reference แบบนั้น จึงอนุญาต
- compiler เพียงต้องตรวจสอบว่าอาร์กิวเมนต์อื่นอาจมี reference ไปยัง
- ในการคืนค่า ไม่สามารถคืน reference
uniqที่ถูกแปลงแล้วเป็นuniqปกติได้- เพราะหลังจากคืนค่าแล้ว การตรวจของ compiler ที่ว่า “ไม่ใช้ตัวแปรที่อาจเป็น alias ภายในขอบเขต” จะไม่ถูกนำมาใช้
- แต่สามารถกำหนด return type เป็น
local uniq Fooได้- ภายใน เมื่อแปลงจาก
mut refเป็นuniq refสิ่งที่ถูกสร้างจริง ๆ จะเป็น local uniq เสมอ - ในกรณีส่วนใหญ่ใช้ได้เหมือน
uniqปกติ แต่เมื่อต้องคืนค่าจำเป็นต้องระบุให้ชัดเจน
- ภายใน เมื่อแปลงจาก
ต้นทุนเชิงการออกแบบและทางเลือก
- Ante สามารถเปลี่ยน reference counted reference อย่าง
Rc Spaceshipให้เป็นuniq Spaceshipชั่วคราวได้โดยไม่มี runtime error - ข้อเสียคือ compiler ต้องไล่ดู type แบบ recursive เพื่อตอบคำถามอย่าง “จาก
EngineสามารถไปถึงSpaceshipได้หรือไม่” - การวิเคราะห์แบบนี้อาจเปราะบาง
- การเพิ่มฟิลด์ให้ struct อาจกลายเป็น breaking API change ได้
- Jake ผู้สร้าง Ante กำลังมองหาวิธีที่ดีกว่าในการรักษาการรับประกันนี้
- แนวทางที่ติดชนิด brand เฉพาะแบบไม่ระบุชื่อให้กับแต่ละ shared mutable type เช่น group borrowing และ Flix references
- แนวทางที่เพิ่ม effect เช่น
Mutates 'aเมื่อเปลี่ยน shared type เพื่อตัดการวิเคราะห์ type ออกไป - แนวทางที่ให้ผู้ใช้ตรวจตอนรันไทม์ว่า reference สองตัวชี้ไปคนละอ็อบเจกต์หรือไม่ หรือให้ unsafe check ที่ห่อด้วย safe API
- แนวทางที่ compiler ติดตามค่าที่ไม่ได้ถูกเก็บทางอ้อมภายใน
Rcจึงไม่อาจถูก alias ได้
- แนวคิดที่คล้าย iso permission ของ Pony หรือสิทธิ์ชั่วคราวที่มองเข้าไปข้างใน struct ได้แต่ห้ามใช้ reference ที่ชี้ออกไปข้างนอก ก็ยังเป็นความเป็นไปได้
- ส่วนที่ยากคือการรักษา ความใช้งานง่าย, ความอ่านง่าย และ ความเรียบง่าย ซึ่งเป็นเป้าหมายของ Ante ไว้ ขณะยังคงความยืดหยุ่นเช่นนี้
กระแสที่กว้างขึ้นของความปลอดภัยหน่วยความจำ
- shared mutable borrowing เคยถูกมองว่าเป็นไปไม่ได้ และมีมุมมองว่า Rust ก็ถูกออกแบบบนความเชื่อนั้น
- ข้อยกเว้นต่าง ๆ กำลังสะสมมากขึ้น
- Ante สามารถได้
uniqborrow reference จากข้อมูล shared-mutable ผ่านกฎ local uniqueness - Vale สามารถได้ immutable borrow reference จากข้อมูล shared-mutable ผ่าน pure function
- group borrowing สามารถสร้าง shared-mutable borrow reference ได้แม้ไม่ใช่ shape-stable
- GhostCell ของ Rust อนุญาตให้กราฟของอ็อบเจกต์ชี้หากันได้อย่างอิสระ แต่ ณ เวลาใดเวลาหนึ่งจะมี mutable reference ได้เพียงหนึ่งตัวไปยังหนึ่งในนั้น
- Ante สามารถได้
- กระแสนี้บ่งชี้ว่าอาจมีหลักการที่ทั่วไปกว่าสำหรับจัดการ shared mutable borrowing ในการออกแบบความปลอดภัยหน่วยความจำ
เปรียบเทียบกับ Cell ของ Rust
- ผู้ใช้ Rust อาจถามว่าการใส่
Cellในฟิลด์ของ struct ต่างจากแนวทางของ Ante อย่างไร - ในตัวอย่าง Ante สามารถได้ reference
mut Stringไปยังstatus: StringจากRc Spaceshipแล้วต่อ" (refueling)"เข้าไปโดยตรง - ในแนวทาง
Cell<String>ของ Rust ไม่สามารถได้&mut StringจากRc<Spaceship>- แต่ต้องใส่ค่าพื้นฐานชั่วคราวด้วย
status_ref.replace(String::new()) - แก้ไข
Stringที่ดึงออกมา - แล้วสุดท้ายใส่กลับด้วย
replace(status)
- แต่ต้องใส่ค่าพื้นฐานชั่วคราวด้วย
- วิธีนี้มีข้อเสียหลายอย่าง
- ต้องสร้าง อินสแตนซ์พื้นฐาน เช่น
"" - มีความเสี่ยงที่จะลืมเรียก
replaceครั้งสุดท้าย - มีความเสี่ยงที่บางคนจะอ่าน
statusขณะที่ค่าถูกแทนที่อยู่
- ต้องสร้าง อินสแตนซ์พื้นฐาน เช่น
- Ante อนุญาตให้ได้ reference ไปยัง string
statusชั่วคราว และ compiler จะบังคับไม่ให้โค้ดอื่นเข้าถึงในระหว่างนั้น
1 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
ที่เคยคิดว่า “shared mutable borrow” เป็นไปไม่ได้นั้น ไม่ใช่แค่ข้อแลกเปลี่ยนที่ Rust ยอมรับเพื่อให้บรรลุเป้าหมาย แต่แทบจะเป็นเป้าหมายแกนหลักของ Rust เอง
เพราะสถานะที่แก้ไขได้และถูกแชร์ร่วมกันทำให้การ ให้เหตุผลเฉพาะที่ กับโค้ดยากขึ้น
"References are like jumps" by withoutboats อธิบายประเด็นนี้ได้ดีมาก ประเด็นสำคัญคือการป้องกันไม่ให้สถานะที่มี alias ถูกแก้ไขโดยไม่ตั้งใจ เพื่อให้สร้างระบบที่ทำงานได้ถูกต้องได้ง่ายขึ้น และกฎ lifetime ของ Rust ก็ไม่ใช่แค่อุปกรณ์สำหรับหลีกเลี่ยง garbage collection แต่เป็นโครงสร้างที่ลึกกว่านั้นเพื่อรักษาความสามารถในการให้เหตุผลในภาษาที่อนุญาตทั้งสถานะแบบ mutable และสถานะแบบ alias พร้อมกัน
ดูค่อนข้างดีทีเดียว
ถ้าเข้าใจไม่ผิด เวทมนตร์ที่เปลี่ยนจาก shared reference ไปเป็น mutable reference ทำได้เพราะจำกัดอยู่กับชนิดที่ไม่ถูกแชร์ข้ามเธรด และความเป็นเอกลักษณ์ของ
Rcก็ดูเหมือนจะรับประกันโดยมองว่าวัตถุทั้งหมดของชนิดเดียวกันถูกยืมด้วย lifetime เดียวกันเรื่องไวยากรณ์แบบชัดเจนหรือแบบเป็นธรรมชาติดีกว่ากันอาจเป็นเรื่องรสนิยม แต่ก็แสดงให้เห็นว่าถ้าคอมไพเลอร์รู้จัก
Cellมากขึ้น ก็อาจอนุญาต mutable reference ไปยังมันได้ยืดหยุ่นกว่าเดิมและยังเลี่ยงการใช้คำที่ทำให้สับสนแบบใน Rust ที่
mutถูกใช้ในความหมายว่า exclusive/unique มากกว่าจะหมายถึง mutableuniqหมายถึงการ acquire lock หรือเปล่า?” แต่ดูเหมือนต้องเข้าใจว่าเขาเทียบกับRcไม่ใช่Arcmutหมายถึง exclusive/unique คืออะไร?อยากรู้ว่ามีใครพอเดาได้ไหมว่า หลักการรวมศูนย์ ที่ผู้เขียนใบ้ไว้ตอนท้ายนั้นคืออะไร
การอภิปรายก่อนหน้านี้เกี่ยวกับโพสต์ในบล็อก antelang.org ก็น่าดูเหมือนกัน
ผมยังไม่ค่อยเข้าใจว่ามันทำงานอย่างไร ดูเหมือนว่ามันจะบอกว่า “ถ้ามี mutable pointer ไปยังออบเจ็กต์หนึ่ง ก็สามารถเอา modifying reference ไปยัง slice ของออบเจ็กต์นั้นได้”
แต่ถ้าอย่างนั้นก็ดูเหมือนจะเขียนอะไรแบบ
mutref someobjext = …,mutref subfield = someobjext.a.b,someobjext.a = somethingelseได้ และถ้าเป็นแบบนั้นsubfieldก็อาจใช้ไม่ได้อีกต่อไป หรือพังเพราะค่าถูกเปลี่ยนไปในบทความมีทั้งคำอธิบาย การเปรียบเทียบกับภาษาอื่น และตัวอย่างโค้ดมากมาย แต่กลับหาส่วนที่สรุป ความหมายเชิงลำดับขั้น ของการทำงานนี้แบบพื้นฐานจริง ๆ ได้ยาก