เนื้อหาในเว็บไซต์นี้ได้รับการแปลโดยใช้ปัญญาประดิษฐ์ (AI) หรือเทคโนโลยีการแปลด้วยเครื่อง และอาจมีข้อผิดพลาด

Skip to content

การอนุมานชนิดข้อมูลใน 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 และบล็อกนี้ไม่ได้รับรองหรือสนับสนุนบริษัทหรือบริการใด ๆ ทั้งสิ้น นอกจากนี้ ไม่มีการรับประกันหรือคำมั่นสัญญาใด ๆ เกี่ยวกับความถูกต้อง ความน่าเชื่อถือ หรือความสมบูรณ์ของข้อมูลที่ปรากฏในบล็อกนี้