Skip to content

RFC: if and while statement initializers (if local and while local statements) #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

deviaze
Copy link

@deviaze deviaze commented Mar 27, 2025

This RFC is an update/continuation/alternative to if statement initializers (#23), with updated fallthrough semantics, the addition of while local statements, and pancakes (stacked local initializations in if local statements).

Rendered.

Basically,

if local index = table.find(list, element) then
    table.remove(list, index)
end

while local line = nextline() in line ~= "" do
    -- do something with line
end

in Roblox examples:

if local character = player.Character then
    -- character is not nil and is bound here
    if local humanoid = character:FindFirstChildOfClass("Humanoid")
        in humanoid.Health < 20
    then
        -- humanoid is not nil, is bound here, and humanoid's health < 20
    else
        -- humanoid is not bound here
    end
end
-- character not bound here

and as a better way of handling nested if locals,

if
    local character = player.Character
    local head = character:FindFirstChild("Head") in notTooFarAway(head.Position)
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
then
    -- condition executes if all the above are guaranteed to be not-nil here and `head` is notTooFarAway
else
    -- handle your fallback cases
end

This RFC turned out quite long, apologies, but the discussion on if statement initializers was pretty long and there's a lot to keep track of and address, especially in terms of alternatives. Please enjoy!

@dphfox
Copy link

dphfox commented Mar 27, 2025

The in syntax doesn't sit right with me, though I like this idea in concept.

Perhaps a semicolon is a more natural statement separator? And perhaps that leads to the notion of those in expressions not actually being special at all, and just acting like and except it can terminate a statement fully (i.e. evaluate a local) and thus let you access previous identifiers.

if
    name ~= nil;
    local player = Players:FindFirstChild(name);
    local character = player.Character;
    local humanoid = character:FindFirstChildOfClass("Humanoid");
    humanoid.Health > 0
then
    humanoid.Health -= 5;
end

A simpler example:

if local match = s:match(REGEX); #match > 2 then
    print("found it!")
end

I think that, while I appreciate the attempt to use a keyword in line with the rest of Luau, that it's better to use a symbol that people can assign meaning to themselves, rather than constructing an explicit phrase that reads wonkily.

@deviaze
Copy link
Author

deviaze commented Mar 27, 2025

The in syntax doesn't sit right with me, though I like this idea in concept.
Perhaps a semicolon is a more natural statement separator? And perhaps that leads to the notion of those in expressions not actually being special at all, and just acting like and except it can terminate a statement fully (i.e. evaluate a local) and thus let you access previous identifiers.

Hmm, in was chosen in if statement initializers last year after a lot of bikeshedding between where vs in vs ;, so I basically adopted it towards this RFC. I can see why it sounds a bit wonky without an OCaml background because you have to explain "it's in because you're putting the identifier into the expression"; I think @aaxte was the person who was most pro-in last time, so maybe she could share her thoughts on the keyword?

Otherwise, I kinda like the idea that you can mix regular if expressions and if locals, especially because it better implies that the local can't be nil, but I also like ins because they're more visually distinct. I'm not a huge fan of putting semicolons everywhere, maybe we could require semicolons only to separate local assignments from standard expressions?

For example:

if
    name ~= nil; -- we don't even need this one tbh
    local player = Players:FindFirstChild(name)
    local character = player.Character
    local humanoid = character:FindFirstChildOfClass("Humanoid");
    humanoid.Health > 0
then
    humanoid.Health -= 5
end

@dphblox
Copy link

dphblox commented Mar 27, 2025

We could adopt the same tack as semicolons in normal Luau; can be used to disambiguate statements, but when there's no ambiguity perhaps there's no need for them.

@deviaze
Copy link
Author

deviaze commented Mar 27, 2025

I'm reminded of a humorous idea from the previous RFC's comments.. in ->if as if guards. It's kinda wack when you're talking about single if local but actually might just make sense with local pancakes...

if
    local character = hit.Parent if character:IsA("Model")
    local hit_player = Players:GetPlayerFromCharacter(model) if hit_player.Team ~= player.Team
    local humanoid = character:FindFirstChildOfClass("Humanoid") if humanoid.Health > 0
then
    humanoid.Health = math.max(0, humanoid.Health - 5)
end

it's definitely parseable in the face of if expressions because you can't chain 2 if expressions without an operator.

Nevermind, it's not parseable without parens. Forgot about if x then a else if y then b else ...

@dphblox
Copy link

dphblox commented Mar 27, 2025

I was going to suggest and if before but it's ambiguous for the same reason.

One thing I'm concerned about wrt in is that it may look like it's part of an expression, especially to people coming from e.g. Python or GDScript:

data = "hello" in fetch(data)
if local data = "hello" in fetch(data) then
    ...
end

And the other usage of in - for loops - doesn't have this kind of left-to-right data transfer at all;

-- Data flow -> -> ->
if local data = "hello" in fetch(data) then

-- Data flow <- <- <-
for key, value in contents do

Nor would any future prospective use of in;

-- Data flow <- <- <-
local { useState, useMemo } in require("react")
local data = { 2, 4, 6, 8 }
-- Data flow <- <- <-
local indexOf = 6 in data
if indexOf ~= nil then
    ...
end

Since we're locked into using reserved keywords or symbols for disambiguity, and we're looking for a statement/expression terminator, I'd rather we stick to something that already matches that function. Semicolons seem to fit that bill, which is why I would prefer those.

@deviaze
Copy link
Author

deviaze commented Mar 27, 2025

That's a really good point on the information flow direction; I was somewhat worried about the same kind of thing when I first saw in clauses and I didn't *get* the reasoning why in would make sense there, but I didn't know how to express it at the time. I'd say that putting variables in an expression is mainly intuitive to OCaml programmers, who use in for left -> right information flow all the time:

let () = 
  let a = 1 in 
  let b = a + 1 in
  print_endline(string_of_int b)

And I'd say Luau programmers would be a lot more familiar with Python, Lua, JavaScript, (and for compiled languages, probably Rust, C/++, etc.) over OCaml, and so in could be confusing when it's an operator in Python and used in for loops in Luau (and other languages).

For what it's worth, we aren't actually locked into existing keywords here; in fact, the previous rfc originally suggested a new conditional keyword where to demark the conditional clause.

Although I like the idea of making the evaluation condition non-special, I'm wondering how that could affect its implementation. From what I know from last time I talked to Luau engineers about this, it was reasonable to special case locals into a control structure, but I'm not sure if it's possible to do that if subsequent locals refer to previous ones, and if non-nil check has to be evaluated one at a time. That's why I suggested that

-- using ; instead of in for ya
if
    local character = hit.Parent; character:IsA("Model")
    local hit_player = Players:GetPlayerFromCharacter(model); hit_player.Team ~= player.Team
    local humanoid = character:FindFirstChildOfClass("Humanoid"); humanoid.Health > 0
then
    humanoid.Health = math.max(0, humanoid.Health - 5)
end

Could expand to

if local character = hit.Parent; character:IsA("Model") then
    if local hit_player = Players:GetPlayerFromCharacter(model); hit_player.Team ~= player.Team then
         if local humanoid = character:FindFirstChildOfClass("Humanoid"); humanoid.Health > 0 then
             humanoid.Health = math.max(0, humanoid.Health - 5)
         end
    end
end

if it's not possible to get a more efficient/bespoke pancake implementation.

Also just looking at the syntax highlighting for the above two.. I kinda want my red ins back 😭

@Bottersnike
Copy link

I really don't like that it does a nilness check not a truth check as the primary condition. Semantics for if are pretty well established that if false then ... end won't execute that block of code and this feels like it would just be confusing. In fact, the given

if local success, result = pcall(foo) then
    if success then

example demonstrates confusion as one might expect the if local to be checking success for us already. Similarly this example is a tautology as pcall will never return nil.

+1 on the dislike of in too for pretty much all the reasons stated above. Along with the confusion with other languages where x in y is a membership check, the existing use of in in luau is as part of iteration over members which is a very different use to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants