การอนุมานชนิดข้อมูลใน Luau

ตั้งแต่ปี 2006 นักพัฒนา Roblox ได้ใช้ภาษาโปรแกรม Lua ในการสร้างเกมและประสบการณ์แบบโต้ตอบบน Roblox นักพัฒนา Roblox มาจากหลากหลายอาชีพและทุกระดับประสบการณ์ และผลงานที่พวกเขาสร้างขึ้นก็มีความหลากหลายไม่แพ้กัน
บางผลงานเหล่านั้นเป็นซอฟต์แวร์ที่มีความซับซ้อนอย่างแท้จริง หลายชิ้นมีโค้ดหลายหมื่นบรรทัด
แม้ว่า Lua จะเป็นภาษาโปรแกรมที่ยอดเยี่ยมและเราชอบมันมาก แต่เราก็ได้เรียนรู้บทเรียนเดียวกับที่ชุมชนนักพัฒนาเว็บได้เรียนรู้: การเขียนแอปพลิเคชันขนาดใหญ่ด้วยภาษาโปรแกรมที่มีการกำหนดประเภทแบบไดนามิกนั้นเป็นเรื่องยาก!
เราสร้าง Luau ขึ้นมาเพื่อเติมเต็มช่องว่างนี้
ที่แก่นแท้ Luau เป็นเครื่องมืออนุมานประเภท (type inference engine) ก่อนเป็นเครื่องมือตรวจสอบประเภท (type checker) เราได้นำปรัชญานี้มาจากผลงานที่ประสบความสำเร็จมาก่อน เช่น คอมไพเลอร์ OCaml และ TypeScript
อย่างไรก็ตาม Lua ไม่เหมือนกับ OCaml หรือ JavaScript อย่างแท้จริง เพื่อให้ Luau ดีที่สุดเท่าที่จะเป็นไปได้ สิ่งสำคัญมากคือเราต้องจำลองสิ่งที่ทำให้ Lua พิเศษได้อย่างถูกต้อง
ก่อนอื่น ขออธิบายพื้นฐานบางอย่าง
การอนุมานชนิดข้อมูลเบื้องต้น
การอนุมานประเภทเกือบจะทั้งหมดเกี่ยวกับการสังเกตว่าประเภท A บางประเภทต้องเหมือนกับประเภท B อื่น ๆ นี่เป็นการทำให้ง่ายขึ้น แต่ก็ไม่ใช่น้อยอย่างที่คิด
มาเริ่มกันด้วยตัวอย่างที่เล็กมากๆ:
function id(x)
return x
end
local a = 5
local b = id(a)เมื่อเราตรวจสอบประเภทของ id เราจะสร้างประเภทตัวที่ถือไว้สองประเภท: ประเภทของ x และประเภทการคืนค่าของฟังก์ชัน
ตัวที่แทนที่เหล่านี้บ่งบอกเพียงสิ่งเดียว: เราไม่รู้อะไรเกี่ยวกับพวกมันเลย
จากนั้นเราจะวิเคราะห์คำสั่ง return เราสังเกตเห็นว่า ไม่ว่าฟังก์ชันจะคืนค่าประเภทใด มันก็เหมือนกับประเภทของ x เนื่องจาก x ไม่มีข้อจำกัดอื่นใดที่ถูกกำหนดไว้ ประเภทสุดท้ายที่อนุมานได้ของ id จึงถูกอนุมานว่าเป็นฟังก์ชันทั่วไป (T) -> T
(Luau ยังไม่มีไวยากรณ์สำหรับฟังก์ชันทั่วไป แต่โครงสร้างข้อมูลภายในสามารถแสดงฟังก์ชันเหล่านี้ได้)
เมื่อถึงเวลาที่ต้องอนุมานประเภทของ b เราสามารถใช้ความรู้เกี่ยวกับ id ที่มีอยู่ได้ เราใช้ประเภทของ id และผูกประเภทพารามิเตอร์ของมันกับประเภทอาร์กิวเมนต์ที่เป็นรูปธรรมที่เรามี เนื่องจากประเภทการคืนค่าของฟังก์ชันถูกผูกกับประเภทพารามิเตอร์ของมันอยู่แล้ว (T) -> T จึงถูกสร้างเป็น (number) -> number จากนี้ เราสามารถอนุมานได้ว่าประเภทของ b คือ number
เกือบทั้งหมดของงานตรวจสอบประเภทที่ Luau ทำเป็นการขยายแนวคิดนี้
การคืนสินค้าหลายรายการ
ในเกือบทุกภาษาโปรแกรมที่ใช้กันอย่างแพร่หลาย ฟังก์ชันจะส่งคืนค่าเพียงค่าเดียวเท่านั้น ภาษาโปรแกรมหลายภาษา (โดยเฉพาะภาษาเชิงฟังก์ชัน) มีการใช้เทป (tuple) ที่มีน้ำหนักเบาเพื่อให้การส่งคืนค่าหลายค่าเป็นเรื่องง่ายและสะดวก
Lua แตกต่างจากแนวโน้มนี้โดยอนุญาตให้ทุกฟังก์ชันส่งค่ากลับได้ 0 ค่าหรือมากกว่า ไม่มีการใช้กลไกของทูเพิลที่นี่ เนื่องจากไม่มีวิธีที่จะผูกทูเพิลทั้งหมดเข้ากับชื่อเดียวได้
function take_five(a, b, c, d, e)
print(a, b, c, d, e)
end
function get_five()
return 1, 2, 3, 4, 5
end
-- a receives 1. The other return values are discarded.
local a = get_five()
-- foo, bar, and baz receive 1, 2, and 3, respectively.
-- Arguments 4 and 5 are discarded.
local foo, bar, baz = get_five()
-- the 5 return values from get_five are passed as
-- parameters to take_five
take_five(get_five())นี่ก่อให้เกิดความท้าทายใหม่ ๆ บางประการ ประเภทของฟังก์ชันการเรียกกลับ (compose) ด้านล่างนี้คืออะไร?
function compose(f, g)
return function(...)
return f(g(...))
end
endในภาษาอื่น ๆ เราจะสามารถสรุปได้ว่าประเภทการคืนค่าของ g ต้องเหมือนกับประเภทของอาร์กิวเมนต์ใน f ในภาษา Lua f ต้องยอมรับจำนวนและประเภทของอาร์กิวเมนต์เหมือนกับที่คืนค่าโดย g
ใน Luau เราแสดงสิ่งนี้ด้วยสิ่งที่เราเรียกว่า type pack ซึ่งมีลักษณะคล้ายกับโครงสร้างข้อมูลที่เราใช้เพื่ออธิบายประเภท แต่จะแสดงถึงประเภท 0 หรือมากกว่านั้น
เช่นเดียวกับประเภท แพ็กประเภทรองรับแนวคิดของตัวแทนที่สามารถผูกกับแพ็กอื่นในภายหลังได้ แพ็กประเภทมีคุณสมบัติอื่น ๆ บางประการ: ความยาวของมันอาจเป็นที่ทราบหรือไม่ทราบก็ได้ และหากเป็นที่ทราบแล้ว อาจมีขนาดคงที่หรือเปลี่ยนแปลงได้
Luau models ทำหน้าที่เป็นคู่ของ type packs: หนึ่งสำหรับรายการอาร์กิวเมนต์และอีกหนึ่งสำหรับค่าที่ส่งคืน หากเราสมมติไวยากรณ์ A... เพื่อระบุ type pack แบบทั่วไป เราสามารถเขียน type สำหรับ compose ได้ดังนี้
:
((B...) -> C..., (A...) -> B...) -> (A...) -> C...
(Luau ยังไม่รองรับไวยากรณ์นี้เช่นกัน กำลังจะมาเร็ว ๆ นี้!)
ตาราง
ตารางมีความสำคัญมากเมื่อเขียนภาษา Lua พวกมันคืออาร์เรย์ แฮชแมป และอ็อบเจ็กต์ทั้งหมดในหนึ่งเดียว จึงไม่น่าแปลกใจที่การอนุมานประเภทของตารางจากโค้ด Lua อย่างถูกต้องนั้นมีความสำคัญมาก
ใน Luau, เราแบ่งตารางออกเป็น 4 หมวดหมู่:
- ตารางที่มีโครงสร้างที่แน่นอนซึ่งเราทราบ
- ตารางที่สร้างขึ้นเป็นส่วนๆ
- ตารางที่คล้ายกันซึ่งถูกส่งผ่านเป็นพารามิเตอร์ของฟังก์ชัน และ
- ประเภทข้อมูลของ Roblox API
เราเรียกตารางเหล่านี้ว่า ตารางปิดผนึก, ตารางไม่ปิดผนึก, ตารางทั่วไป, และคลาสเนทีฟ
โต๊ะปิด
ข้อผิดพลาดที่พบได้บ่อยมากที่เราต้องการสามารถตรวจจับได้คือชื่อคุณสมบัติที่สะกดผิดในคำสั่งกำหนดคุณสมบัติของตาราง
local some_table = {some_property=0}
-- oops. I got the name of the property wrong
some_table.sone_property = 55โดยทั่วไปแล้ว โต๊ะจะถูกปิดผนึกไว้เป็นค่าเริ่มต้น
โต๊ะที่ไม่ได้ปิดผนึก
เพื่อให้ทำงานได้อย่างราบรื่นกับภาษา Lua ที่เป็นธรรมชาติ เราจำเป็นต้องมีวิธีในการรองรับฟังก์ชันและโมดูลที่สร้างตารางขึ้นมาจากหลายคำสั่ง
local Counter = {}
Counter.value = 0
function Counter.increment()
Counter.value = Counter.value + 1
return Counter.value
endในตัวอย่างนี้ จะเป็นการไม่ฉลาดหากเราดูที่บรรทัดแรก, สรุปประเภท {} และทำให้เกิดข้อผิดพลาดของประเภทในบรรทัดที่สอง เพราะเราคาดหวังว่า Counter จะยังคงว่างเปล่าตลอดไป
ดังนั้นเราจึงมีจุดยืนว่า Counter เป็นตารางที่ไม่ได้ปิดผนึก Luau สามารถทราบรูปร่างที่แน่ชัดของตารางนี้ได้อย่างง่ายดาย แต่เราถือว่าตารางนี้สามารถขยายได้
เราไม่ต้องการให้โต๊ะถูกยกเลิกการปิดผนึกได้ง่ายเกินไป ดังนั้นเราจึงใช้หลักการฮิวริสติกส์อย่างง่าย:
- ประเภทของตารางจะถูกยกเลิกการปิดผนึกเมื่อมีการเริ่มต้นด้วยตารางว่างเปล่าโดยตรง และ
- ตารางที่ไม่ได้ปิดผนึกจะถูกแปลงเป็นตารางที่ปิดผนึกทุกครั้งที่เราพบในลายเซ็นของฟังก์ชัน
สิ่งนี้ทำให้เราได้รับประสบการณ์การใช้งานที่ดีทีเดียว
function new_counter()
local Counter = {}
Counter.value = 0 -- OK. Counter is unsealed.
function Counter.increment()
Counter.value = Counter.value + 1
return Counter.value
end
return Counter
end
local c = new_counter()
c.value_ = 5 -- Not allowed. c is a sealed table here.ตารางทั่วไป
เมื่อจัดการกับพารามิเตอร์ของฟังก์ชันที่ไม่มีการระบุข้อมูลเพิ่มเติม มักจะเป็นไปได้ยากที่จะระบุรูปแบบที่แน่นอนของอาร์กิวเมนต์ที่ใช้ในรูปแบบตาราง:
local function print_point(p)
print(‘X =’, p.X, ‘Y =’, p.Y)
endเราสามารถทราบได้ว่า p มี X และ Y แต่เนื่องจากฟังก์ชัน print สามารถพิมพ์อะไรก็ได้ นั่นคือทั้งหมดที่ทราบได้ คุณสมบัติเหล่านั้นอาจมีประเภทใดก็ได้ อาจมีคุณสมบัติอื่น ๆ อีกหลายรายการก็ได้ และไม่จำเป็นต้องเป็นตารางจริง ๆ ด้วยซ้ำ อาจเป็นประเภท API ของ Roblox เช่น Vector3 ก็ได้
(เพิ่มเติมเกี่ยวกับ Roblox API ในอีกสักครู่)
เช่นเดียวกับ OCaml และภาษาโปรแกรมบางชนิด พารามิเตอร์ตารางใน Luau เป็นพหุรูปแบบแถว ใน Luau เราเรียกสิ่งเหล่านี้ว่าตารางทั่วไป ฟิลด์ที่อนุมานว่ามีอยู่ในตารางทั่วไปจะเป็นข้อกำหนดที่บังคับใช้กับผู้เรียก ฟีเจอร์อื่น ๆ สามารถใช้ได้ตราบใดที่มีโครงสร้างที่ต้องการ:
local a = print_point({X=3, Y=4})
local c = print_point({X=4, Y=3, Name='The Best Point'})
local b = print_point(Vector3.new(3, 4, 0))Roblox API
Roblox API มอบเครื่องมือที่ทรงพลังมากมายให้กับชุมชนนักพัฒนาที่มีความทะเยอทะยานของเรา API ประกอบด้วยคลาส C++ จำนวนมากที่ได้ถูกสะท้อนเข้าสู่ Lua
เป็นที่ชัดเจนว่า Luau จำเป็นต้องรับรู้เกี่ยวกับ API นี้ ส่วนหนึ่งของการรับรู้คือการรู้ว่าอินสแตนซ์ของคลาสใน Roblox ไม่ใช่ตาราง Lua จริงๆ ตัวอย่างเช่น ฟังก์ชัน pairs() ที่ติดตั้งมาในตัวไม่สามารถใช้เพื่อวนซ้ำคุณสมบัติของประเภท Roblox API ได้
ระบบประเภทที่เราได้อธิบายไว้สำหรับ Luau จนถึงตอนนี้เป็นระบบประเภทเชิงโครงสร้างอย่างสมบูรณ์ Lua (และ Luau) ถือว่าตารางเป็นเพียงชุดของคุณสมบัติที่มันถืออยู่เท่านั้น ไม่มากหรือน้อยไปกว่านั้น Roblox API ไม่เหมาะกับโมเดลนี้ C++ ให้ระบบประเภทนามธรรมที่ทุกคลาสมี "ความเป็นตัวเอง" ของตัวเอง เป็นเรื่องปกติอย่างยิ่งที่จะมีสองคลาสที่แตกต่างกันแม้ว่าจะมีโครงสร้างเหมือนกันทุกประการก็ตาม มีคลาสของ Roblox จริงๆ ที่ตรงกับคุณสมบัตินี้และ Luau จำเป็นต้องจำลองพวกมันอย่างถูกต้อง
เราแก้ไขปัญหานี้โดยการแนะนำประเภทที่มีลักษณะคล้ายตารางสำหรับอินสแตนซ์ของคลาสในตัวของ Roblox ประเภทของคลาสแตกต่างจากประเภทของตารางตรงที่มีเอกลักษณ์เฉพาะตัวที่ทำให้สามารถแยกแยะได้แม้ว่าจะรองรับเมธอดทั้งหมดด้วยประเภทเดียวกันก็ตาม นอกจากนี้ยังรองรับแนวคิดของการสืบทอดเช่นเดียวกับคลาสใน C++ ที่ถูกสร้างขึ้นเพื่อจำลอง
สรุป
การดึงประเภทข้อมูลแบบคงที่ออกจากภาษาแบบไดนามิกอย่าง Lua นั้นมีความท้าทายมากมาย ความท้าทายหลายอย่างเหล่านี้เฉพาะเจาะจงกับ Lua แต่ก็สามารถแก้ไขได้ค่อนข้างดี เราคิดว่ามันทำงานได้ค่อนข้างดีทีเดียว! 🙂
แอนดี้ ฟรีเซน เป็นหัวหน้าฝ่ายเทคนิคสำหรับตัวตรวจสอบประเภท Luau เขารู้สึกตื่นเต้นที่ได้ทำงานในจุดตัดระหว่างวิดีโอเกม เครื่องมือสำหรับนักพัฒนา และภาษาการเขียนโปรแกรม
ทั้งบริษัท Roblox Corporation และบล็อกนี้ไม่ได้รับรองหรือสนับสนุนบริษัทหรือบริการใด ๆ ทั้งสิ้น นอกจากนี้ ไม่มีการรับประกันหรือคำมั่นสัญญาใด ๆ เกี่ยวกับความถูกต้อง ความน่าเชื่อถือ หรือความสมบูรณ์ของข้อมูลที่ปรากฏในบล็อกนี้


