Luau'da Tip Çıkarım

2006 yılından bu yana, Roblox geliştiricileri Roblox üzerinde oyunlar ve etkileşimli deneyimler oluşturmak için Lua programlama dilini kullanmaktadır. Roblox geliştiricileri her kesimden ve her deneyim seviyesinden gelmektedir ve yarattıkları eserler de aynı derecede çeşitlidir.
Bu yaratımların bazıları gerçekten sofistike yazılım parçalarıdır. Birçoğu on binlerce satır koddan oluşur.
Lua harika bir programlama dili ve biz onu çok seviyoruz, ancak web geliştirici topluluğunun öğrendiği dersin aynısını biz de öğreniyoruz: dinamik tiplemeli programlama dilleriyle büyük uygulamalar yazmak zordur!
Bu boşluğu doldurmak için Luau'yu yarattık.
Temelde Luau, öncelikle bir tür çıkarım motoru, ikinci olarak da bir tür denetleyicisidir. Bu felsefeyi, OCaml ve TypeScript derleyicileri gibi önceki başarılı çalışmalardan alıyoruz.
Ancak Lua, OCaml veya JavaScript ile tam olarak aynı değildir. Luau'yu olabildiğince mükemmel hale getirmek için, Lua'yı özel kılan unsurları doğru bir şekilde modellememiz çok önemlidir.
Öncelikle, bazı temel kavramları açıklayalım.
Tip Çıkarımına Giriş
Tip çıkarım, neredeyse her zaman A tipinin B tipiyle aynı olması gerektiğini gözlemlemekle ilgilidir. Bu bir basitleştirmedir, ancak sanıldığı kadar da basit değildir.
Çok küçük bir örnekle başlayalım:
function id(x)
return x
end
local a = 5
local b = id(a)id'nin türünü kontrol ettiğimizde, iki yer tutucu tür oluştururuz: x'nin türü ve fonksiyonun dönüş türü.
Bu yer tutucular tek bir şeyi gösterir: Onlar hakkında hiçbir şey bilmiyoruz.
Ardından return ifadesini analiz ederiz. Fonksiyonun hangi türü döndürürse döndürsün, bunun x'un türüyle aynı olduğunu gözlemleriz. x üzerinde başka bir kısıtlama bulunmadığından, id'un nihai türetilen türü, (T) -> T genel fonksiyonu olarak türetilir.
(Luau henüz jenerik fonksiyonlar için sözdizimi sağlamamaktadır, ancak iç veri yapıları bunları temsil edebilir.)
b için bir tür çıkarmanın zamanı geldiğinde, id hakkındaki bilgimizi kullanabiliriz. id'nin türünü alır ve parametre türünü elimizdeki somut argüman türüne bağlarız. Fonksiyonun dönüş türü zaten parametre türüne bağlı olduğundan, (T) -> T, (number) -> number olarak örneklenir. Bundan, b'nin türünün sayı olduğunu çıkarabiliriz.
Luau tarafından yapılan tip denetimi işlerinin neredeyse tamamı bu fikrin bir uzantısıdır.
Çoklu Dönüşler
Yaygın olarak kullanılan hemen hemen tüm programlama dillerinde, fonksiyonlar tam olarak tek bir değer döndürür. Birçok dil (özellikle fonksiyonel diller), çoklu dönüşleri kolay ve kullanışlı hale getirmek için hafif tuple'lar sunar.
Lua, her fonksiyonun 0 veya daha fazla değer döndürmesine izin vererek bu eğilime ters düşer. Tüm tuple'ı tek bir isme bağlamanın bir yolu olmadığı için burada tuple mekanizması devreye girmez.
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())Bu durum bazı yeni zorluklar ortaya çıkarır. Aşağıdaki compose'nin türü nedir?
function compose(f, g)
return function(...)
return f(g(...))
end
endDiğer dillerde, g'in dönüş türünün f'un argüman türüyle aynı olması gerektiği sonucuna varabiliriz. Lua'da ise f, g tarafından döndürülenlerle aynı argüman sayısını ve türlerini kabul etmelidir.
Lua'da bunu, tür paketi olarak adlandırdığımız bir şeyle temsil ederiz. Bu, bir türü tanımlamak için kullandığımız veri yapısına çok benzerdir, ancak 0 veya daha fazla türü temsil eder.
Türler gibi, tür paketleri de daha sonra diğer paketlere bağlanabilen yer tutucular kavramını destekler. Tür paketlerinin başka özellikleri de vardır: Uzunlukları bilinen veya bilinmeyen olabilir ve biliniyorsa, sabit veya değişken boyuta sahip olabilirler.
Luau, fonksiyonları bir çift tip paketi olarak modeller: Biri argüman listesi, diğeri dönüş değerleri için.
Genel bir tip paketini belirtmek için A... sözdizimini varsayarsak, compose için bir tip yazabiliriz:
((B...) -> C..., (A...) -> B...) -> (A...) -> C...
(Luau henüz bu sözdizimini desteklemiyor. Yakında eklenecek!)
Tablolar
Tablolar, Lua yazarken çok önemlidir. Tablolar, dizilerimiz, hash haritalarımız ve nesnelerimizi tek bir yerde birleştirir. Lua kodundan tablo türlerini doğru bir şekilde çıkarmak oldukça önemlidir.
Luau'da tabloları 4 kategoriye ayırıyoruz:
- Tam yapısını bildiğimiz tablolar
- Parça parça oluşturulan tablolar
- Fonksiyon parametreleri olarak geçirilen tablo benzeri yapılar ve
- Roblox API veri türleri
Bunlara kapalı tablolar, açık tablolar, genel tablolar ve yerel sınıflar diyoruz.
Kapalı tablolar
Yakalamak istediğimiz çok yaygın bir hata, tablo özellik atamasında yanlış yazılmış bir özellik adıdır.
local some_table = {some_property=0}
-- oops. I got the name of the property wrong
some_table.sone_property = 55Tablolar genellikle varsayılan olarak mühürlenmiştir.
Mühürsüz tablolar
Lua'nın kendi diline uygun şekilde sorunsuz çalışabilmesi için, birden fazla deyim üzerinden tablolar oluşturan işlevleri ve modülleri destekleyecek bir yönteme ihtiyacımız vardır.
local Counter = {}
Counter.value = 0
function Counter.increment()
Counter.value = Counter.value + 1
return Counter.value
endBu örnekte, ilk satıra bakıp {} türünü çıkararak, Counter'in sonsuza kadar boş kalacağını beklediğimiz için ikinci satırda bir tür hatası üretmek aptalca olurdu.
Bu nedenle, Counter'in mühürsüz bir tablo olduğu varsayımından hareket ediyoruz. Luau, bu tablonun tam şeklini kolayca bilebilir, ancak biz onu genişletilebilir olarak değerlendiriyoruz.
Bir tablonun mühürsüz hale gelmesini çok kolay hale getirmek istemediğimizden, bazı basit sezgisel kurallar uygularız:
- Bir tablonun türü, boş bir tablo ile başlatıldığında mühürsüz olur ve
- Fonksiyon imzasında mühürsüz bir tablo ile karşılaştığımızda, bu tablo mühürlü tabloya dönüştürülür.
Bu bize oldukça iyi bir kullanılabilirlik sağlar.
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.Genel Tablolar
Anotlanmamış işlev parametreleriyle uğraşırken, tablo benzeri bir şekilde kullanılan bir argümanın tam şeklini belirlemek nadiren mümkündür:
local function print_point(p)
print(‘X =’, p.X, ‘Y =’, p.Y)
endp'in X ve Y özelliklerine sahip olduğunu bilebiliriz, ancak print işlevi her şeyi yazdırabileceğinden, bu kadar. Bu özellikler herhangi bir türde olabilir. Başka özellikler de herhangi bir sayıda mevcut olabilir. Aslında bir tablo olması bile gerekmez; Vector3 gibi bir Roblox API türü de olabilir.
(Roblox API hakkında birazdan daha fazla bilgi vereceğiz)
OCaml ve diğer bazı programlama dillerinde olduğu gibi, Luau tablo parametreleri satır polimorfiktir. Luau'da bunlara genel tablolar diyoruz. Genel bir tabloda mevcut olduğu çıkarılan alanlar, çağrı yapanlara dayatılan gerekliliklerdir. Gerekli yapı mevcut olduğu sürece diğer özelliklere izin verilir:
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, hırslı geliştirici topluluğumuzun hizmetine oldukça güçlü araçlar sunar. API, Lua'ya yansıtılmış oldukça fazla sayıda C++ sınıfından oluşur.
Luau'nun bu API'yi bilmesi gerektiği açıktır. Bu bilginin bir parçası, Roblox sınıf örneklerinin aslında Lua tabloları olmadığıdır. Örneğin, yerleşik pairs() işlevi, bir Roblox API türünün özellikleri üzerinde yineleme yapmak için kullanılamaz.
Şimdiye kadar Luau için tanımladığımız tür sistemi, tamamen yapısal bir tür sistemidir. Lua (ve Luau), tabloları, içerdikleri özellikler kümesinden başka bir şey olarak görmez. Roblox API'si bu modele uymaz. C++, her sınıfın kendi "özelliğine" sahip olduğu bir nominal tip sistemi sağlar. Tamamen aynı yapıyı paylaşmalarına rağmen birbirinden farklı iki sınıfa sahip olmak tamamen tipiktir. Bu özelliği karşılayan gerçek Roblox sınıfları vardır ve Luau'nun bunları doğru bir şekilde modellemesi gerekir.
Bu sorunu, yerleşik Roblox sınıf örnekleri için tablo benzeri bir tür getirerek çözüyoruz. Sınıf türleri, aynı türlerle aynı yöntemlerin tümünü destekleseler bile onları ayıran kimliklere sahip olmaları bakımından tablo türlerinden farklıdır. Ayrıca, modellemek üzere tasarlandıkları C++ sınıfları gibi miras kavramını da desteklerler.
Sonuç
Lua gibi dinamik bir dilden statik türler çıkarmak birçok zorluk getirir. Bu zorlukların çoğu Lua'ya özgüdür, ancak çözülebilir niteliktedir. Bizce oldukça iyi çalışıyor! 🙂
Andy Friesen, Luau tip denetleyicisinin teknik lideridir. Video oyunları, geliştirici araçları ve programlama dillerinin kesiştiği noktada çalışmaktan büyük heyecan duymaktadır.
Ne Roblox Corporation ne de bu blog, herhangi bir şirketi veya hizmeti onaylamakta ya da desteklemektedir. Ayrıca, bu blogda yer alan bilgilerin doğruluğu, güvenilirliği veya eksiksizliği konusunda hiçbir garanti veya taahhüt verilmemektedir.


