Luau Recap: October 2020

For crashes, just make a module that is something like this:

export type MyClass = TEMP

local module = {} --on a side note i don't think it exports properly if not named module..?
do
    local meta = {__index = {
        Print = function(self: MyClass)
            print(self.Text)
        end
    }}
    module.new = function(text: string?): MyClass
        return setmetatable({
            Text = text or ""
        }, meta)
    end
end

type TEMP = typeof(module.new())

return module

And then just try to use the type in a script that directly requires it and it will likely crash when creating a new object of that class and attempting to use it, as well as index its properties.
example:

local MyClass = require(script.MyClass)
local obj: MyClass.MyClass = MyClass.new()
print(obj.Text)
print(obj.Print)
print(obj.fakeProperty)

Appears to be a crash that is caused from evaluated types such as when using typeof or executing other things that cant be directly resolved. Either instantly crashes studio or displays the dreaded “we’re sorry! roblox needs to close!” without any save prompts

I would also like to see automatic OOP type recognition. For example, I could do something like this:

typeOOP MyClass
and that would allow you to just use MyClass as a type, and when importing the module you could just do this:

local obj: MyClass = MyClass.new()
-- or automatically deduce
local obj = MyClass.new()

although I wonder how type transferring works if you call a function served from another script that was given a return type that has not been established in the current script.

also, this works somehow…?

AnimSet.new = function(): AnimSet
	return setmetatable({X = 2}, meta)
end

export type AnimSet = AnimSet --this actually works, I never declared any type with AnimSet before

It would also be really neat to see type casting for table construction, for example:

type myObjBase = {
    x: number
}
--...
return setmetatable({
    x = myValue or 5
}: myObjBase, meta)

or

--...
return setmetatable({
    x: number = myValue or 5
}, meta)
--should support ["x"]: number = ...

just to ensure the appropriate type is set for the object without needing to do this:

local tbl: myObjBase = {
    x = myValue or 5
}
return setmetatable(tbl, meta)
1 Like

I agree. It’s very disheartening that OOP is currently only in the consideration stage for a future version of Luau (which could be a year or so away with them saying “probably after v1”).

This is literally the reason why I am not using any of this new syntax in my project, I use the idiomatic Lua class syntax and there is no elegant way to get custom types for those classes to behave nicely.

I also don’t like one of the ideas I saw for doing this in another topic which suggested having to define the type by calling the constructor of your class like so:

type MyObject = typeof(MyObject.new())

This is terrible if you have constructors that take args that are another custom class or a complex table that can’t really be initialised at that point or an instance of the class expects a certain game state.

2 Likes

Would likely be better to have a type service where you can register a type like

TypeService:Add(“MyCoolObj”)
TypeService:New(“MyCoolObj”, self)

Then at least you could set the object to be recognized as that type during construction.

I dunno, I probably won’t be using typed any time soon as I have some really steller systems in place for OOP but, I may get to use it on my function libraries.

Is this supposed to happen or is it a bug? Also strings lost their color
RobloxTypeCheckingBUg'

It is because as previously discussed, it checks too much for all of the potential types.
I am not sure why you are using BasePart | string anyway since Instance.new() returns Instance (and in this case, Part), but since your type involves potentially being a string, it checks and sees that .Color isnt a valid key to string types.
I recommend instead using either of these:

local Button: Instance = Instance.new("Part") -- it will auto deduce to this anyway
local Button: Part = Instance.new("Part")
1 Like

Some more suggestions @zeuxcg

I think my favorite suggestions so far though are for table type creation on the fly and is and as operators

local myClassObj = setmetatable({
    x: number = value or 5,
    y: number = 10,
}, classMeta)

that way type deduction should be easier.
It would be interesting though to see some other way of defining metatables that is more static and conventional with other languages be introduced into LuaU, as the structure of script created objects are one of Lua’s many downsides.
example:

class Object {
    Name: string,
    new = function(self, name: string?)
        self.Name = name or ""
    end,
    tostring = function(self): string -- or __tostring, more akin to python structure
         return self.Name
    end,
    Method = function(self, data) -- it should deduce self as being Object
        print(self.Name, "hello world", data)
    end
}

As for function templating, consider a schema like this:

Ptr.new = function<T>(value: T): Ptr<T>
  return {
    Value = value
  }
end

-- or

Ptr.new = function<T>(value: T): Ptr<T>
  return {
    Value: T = value
  }
end

which would allow easy type deduction

local Val1 = Ptr.new(2) -- auto deduce Ptr<number>
local Val2: Ptr<Instance?> = Ptr.new(MyPart)

Val1.Value = 10 -- allowed
local result: {} = Val1.Value -- not allowed, incorrect type matching
local exists: boolean = Val2.Value ~= nil -- valid given the type
if exists then
    local object: Instance = Val2.Value -- assume is Instance, or use "Val2.Value as Instance"
    if object is BasePart then
        print((object as BasePart).CFrame) -- not necessary but can make it easier to deduce type and conversions as well as error if the casting is invalid
    end
end

This is another reason for the huge usefulness of a as operator at runtime. It could error for invalid type casting conversions without even changing the value. Something like nil as Instance should throw an error, as well as provide warnings if the deduced type will never follow that typing. And you could also introduce is as a replacement for both typeof and IsA:

print(typeof(myObj) == typeof(BasePart)) --clunky
print(typeof(myObj) == "BasePart") --still clunky
print(myObj is BasePart)

is could even detect inherentence if you so choose:

if obj is Instance then
    -- potentially implicit cast here if the type is Instance? or (Instance | number) or whatever
    print(obj.Name) -- should work for baseparts or models or whatever, an alternative to IsA for different hierarchies
    -- more type friendly versions:
    print((obj as Instance).Name)

    local thing = obj as Instance
    -- alternative: [local thing: Instance = obj] call "as" automatically for differing types
    print(thing.Name)
end

Usage in a templated function:

local function getChildren<T>(obj: Instance): Array<T>
    local list: Array<T> = {} -- extension of template into function body as local type
    for _, child: Instance in next, obj:GetChildren() do
        if child is T then
            list[#list + 1] = child as T -- !IMPORTANT!, "as" is always only a type casting, it does not convert the object to be a BasePart class statically
        end
    end
    return list
end

local parts: Array<Part> = getChildren<Part>(MyModel)
for _, obj: BasePart in next, getChildren<BasePart>(MyModel) do
    print(obj.CFrame) -- yes ik u can cut it down to only 1 loop by using "is" in here or with an iterator, but just an example
end

local mixed = getChildren<HingeConstraint | CylindricalConstraint>(MyCarJoints)

Example errors (console, with the types checked internally from deduction or manual assignment):

> =workspace as number
print(workspace as number):1: Invalid type coercion (Workspace to number)

> local obj: Instance? = nil; print(obj as Instance)
local obj: Instance? = nil; print(obj as Instance):1: Invalid type coercion (nil to Instance)

> local obj: Instance? = workspace; print(obj as Instance)
Workspace

> local obj: Instance? = nil; if obj is Instance then print(obj as Instance) end
> -- no output above

You can think of is as “check if type coercion is legal” and as as “perform type coercion”

1 Like

I’m LOVING this. It genuinely makes me shudder with excitement to think about something like parallelization and some Lua 5.3 ports (string.pack family in particular,) and I don’t believe you guys will stop here. Keep it up; we love to see this kind of progress. :grin:

Anyways, here’s what I wanted to share today:

String packing functions can be used as a method; but it’s a bit weird, let me show you!

All string packing functions are strayed off the “standard” string library argument scheme, but let’s focus on unpack. Instead of "s,fmt[,pos]" it uses the scheme of "fmt,s[,pos]" > simplified:(strFormat, stringToUnpack) and due to this we can deduce that we cannot pack a string and then use ":unpack(format)" on it due to the fact it needs “format” before “self.”
I bet you’re all tired of reading my single paragraph so let’s get on with it.

local Format = “I1” – unsigned int of length 1 byte (8 bits) (0-255)
local Integer = 212 – int to pack
local Packed = string.pack(Format, Integer)
local Unpacked = Packed:unpack(Format)
print("Packed: " … Packed)
print("Unpacked: " … Unpacked)

Error: invalid format option ‘�’

Now, let me show you the fix for this.

local Format = “I1” – unsigned int of length 1 byte (8 bits) (0-255)
local Integer = 212 – int to pack
local Packed = string.pack(Format, Integer)
local Unpacked = Format:unpack(Packed)
print("Packed: " … Packed)
print("Unpacked: " … Unpacked)

Packed: �
Unpacked: 212

Now the code functions as expected!

But… Why is this so?
Well, when we tried Packed:unpack(Format) we were passing out variable Packed as the first argument, and Format as the second argument (methods send self (The object being indexed) first for the lua illiterate) and this produced an error since we ended up passing “Packed” as our format and that’s not a valid format.
See: Lua 5.3 Official ‘string.pack’ Format for more information on formats.
Moving on, when we tried Format:unpack(Packed) we were passing the proper variables:
string.unpack parameter scheme: <string> Format, <string> self
Which, according to our statement Format:unpack(Packed) is correct, as this would be extended into Format.unpack(Format, Packed) but now we’ve compressed it and saved a good chunk of characters, as well as learned how to use non-method-oriented library functions as a method using a hacky little trick!

Note; my internet cut out like 3 times trying to write this resulting in it taking forever.
I also accidentally posted this in the wrong luau recap a minute ago :smile:

TLDR: string.unpack and string.pack can be used as methods with <string> PackFormat:(un)pack(<string> RawOrPacked, <tuple> ElementsToPack)

EDITS
  • Added functional code output blockquote.
1 Like

string packing functions are really helpful though, very great for making your own file formats or even reading from other file formats if you wish to store some sort of manually serialized data or load and store something like an animation with little to no space used and no longer have headaches from trying to manually implement things from signed and unsigned integers to floating point values (although VLQ (variable length quantities) still must manually be read, it is still a lifesaver in other scenarios). I hope to see compression algorithms like gzip and LZW and such being supported or appended to roblox API as well.

2 Likes

Just a question I had as i’m adding type-checking to my game. How big of a performance boost would doing this give?

At the moment the performance difference should be negligible for most things outside of using pairs/next/ipairs but eventually when LuaU gets further implemented it will improve performance quite a bit because they could optimize memory usage and storage to reduce the amount of overhead from dynamic typing and runtime type checking. for now in its beta stage I don’t recommend overusing it, especially in cases like with OOP where it can cause crashes and just doesn’t quite work right, but in other cases it is good to play with

1 Like

Hi I’ve been just recently started using Luau and whilst working with it I’ve encountered a few problems and issues:

I’ve tried making a metatable a type but ever since I’ve done that any object with the type Unit crashes studio. If I change the return type of the __MakeUnitStatsMetatable to any it stopped the crashing. But then I type unit.Health it warns me with: W0000: Not a table: nil. Which is misleading because when I test it out it works perfectly fine.
image
Another issue I found was that the warnings warn me about an index being nil, while it can’t actually be nil.
image
Storing an variable with type Model (or any other instance) and then trying to index a child of that variable will result in it trying to warn me that x isn’t a key of y. Which is true but I’m not trying to access a key but a child of that instance.
image

Lastly, and this isn’t an bug but a suggestion, is there a way to give table indexes a type? I have modules which have properties and I would love to give those properties a type but I haven’t found a way to do it. And if there isn’t a way to do that now, will there be in the future?

This is the kind of thing I want but as you’ll have read above, they’re not focusing on OOP related stuff at this time.

Really, we need to rally as much support as possible for OOP support in Luau to try and get this feature prioritised. Luau types are really awkward to use with custom classes and prevent you from really going fully type safe.

1 Like

Would you be able to upload the file that causes a crash? We’ve fixed a couple crashes along the same lines but I’d like to make sure your crash is taken care of as well.

The truthfulness checks and child lookups are on our radar for after v0.

As for table indexes, can you explain what you’re trying to do? Maybe a code example would help.

Just to be clear, we aren’t unaware of the need for this. But there’s a certain sequence to the type checking project, and while better support for OOP is high on the list it’s below a few other things that are also pretty fundamental that we don’t support right now.

We’ll share the current plans soon.

5 Likes

Sadly I’ve already made changes to the file and it seems to be crash free now after I removed the metatables. My general experience with metatables + luau has been very glitchy to say the least. Studio often crashes at random when working with them as a type / modifying a member of that type.

As in what I was trying to do with the tables;
I have a module like:

export type item = {x: number, t: string}

local mod = {}
mod.Stack = {} --I would like to give it the type '{item}' but
--writing mod.Stack: {item} = {} will error

function mod.Add(i: item)
	mod.Stack[#mod.Stack + 1] = i
end

return mod

Now I found a way to do like like:

export type item = {x: number, t: string}
export type MOD = {Stack: {item}, Add: (item) -> nil}

local mod: MOD
mod = {
	Stack = {},
	Add = function(i: item)
		mod.Stack[#mod.Stack + 1] = i
	end
}

return mod

But I making the module it self a type does not fit in well with me at all and I would much rather prefer doing mod.Stack: {item} = {}. The module it self shouldn’t be a type I just want a member of that module to have a type, but it is either module is a type or members don’t get a type.

1 Like

While I definitely think mod.Stack: {item} = {} should be valid syntax for Luau, you can rely a bit more on type inference with the following workaround:

export type item = {x: number, t: string}

local mod = {}
do
  local _stack: {item} = {}
  mod.Stack = _stack
end

function mod.Add(i: item)
	mod.Stack[#mod.Stack + 1] = i
end

return mod

It kind of sucks that you have to add another assignment statement (at runtime) for something that should only affect static information about the code, but luau is still in an early state.

1 Like

This is likely intended or at least a known issue, but the following code pattern produces an annoying Type mismatch warning:

If a variable has no type annotation, or is known to be a type of any, changing from one type to another should not produce a warning.

Should I file this as a separate bug report thread, or is it okay for me to post it here?

3 Likes
  1. Am I doing something wrong here?
type Pair<T, U> = {T|U}
local pair: Pair<number, string> = {4, "asdf"}
-- W000: Type 'string' cannot be converted into 'number'
  1. Is there a way to define positional arrays so I can force my Pair type to be two elements of two different types? type Pair<T, U> = {T, U} does not work.

Edit: Seemingly related:

local x: {number|number} = {1, 2, 3}
local y = x[1] - x[2] -- Type 'number|number` could not be converted into 'number'
2 Likes

Another bug report (because I can’t post in Studio Bugs for some reason):

Vector2:Cross is typed to return a Vector2, when in fact it returns a number.

1 Like