Skip to content
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

RFC: If statement initializers #23

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions docs/syntax-if-statements-initializers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Initializers in if statements

# Summary

Introduce an initializer expression that declares and initializes variables in if statements.

# Motivation

Initializers can improve code clarity and reduce scope pollution by allowing developers to declare variables in the conditions of if statements. ⁤⁤ Declaring variables at the point of use in if statements for the scope of the if statement's blocks simplifies code, leading to better readability and understanding of program logic. By limiting the scope of variables to the if statement's blocks, the risk of unintended variable reuse and naming conflicts is reduced. ⁤

The reduced scope pollution improves register space in extreme cases (or auto-generated code) where developers have many variables defined and have to work around register limits by reducing the register size. In some scenarios, especially on Roblox, initializing variables within if statements can lead to improved performance and reduced complexity by avoiding unnecessary calls by developers. ⁤ A common paradigm used by Roblox developers is to use `Instance:FindFirstChild` in their condition and then access it afterwards instead of keeping an existing variable around in the new scope, polluting the existing scope.

Another benefit provided by initializers is being able to compact verbose guard clauses into a singular if statement.

Example:

```lua
function PlayerControl:GetEntities()
if self.RemovedSelf then
return self.Instances
end

local entity = self:TryGetEntity()
if entity == nil then
return self.Instances
end

local index = table.find(self.Instances, entity.Root)
if index == nil then
return self.Instances
end

table.remove(self.Instances, index)
self.RemovedSelf = true

return self.Instances
end
```

```lua
function PlayerControl:GetEntities()
if self.RemovedSelf then
elseif local entity = self:TryGetEntity() in entity == nil then
elseif local index = table.find(self.Instances, entity.Root) then
table.remove(self.Instances, index)
self.RemovedSelf = true
end

return self.Instances
end
```

# Design

If statements with initializers must match the below grammar. The variables declared by an initializer are only available to the if statement's blocks; any code after the if statement won't have the variables defined.

```diff
stat = varlist '=' explist |
...
'repeat' block 'until' exp |
- 'if' exp 'then' block {'elseif' exp 'then' block} ['else' block] 'end' |
+ 'if' cond 'then' block {'elseif' cond 'then' block} ['else' block] 'end' |
'for' binding '=' exp ',' exp [',' exp] 'do' block 'end' |
...
+ cond = 'local' binding '=' exp ['in' exp] |
+ 'local' bindinglist '=' explist 'in' exp |
+ exp
```

In the former case, the value of the first declared variable will be checked.

Example:

```lua
local function foo()
return true
end

if local b = foo() then
print(b, "truthy block")
else
print(b, "falsy block")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to add that this design point is one we have to live with in perpetuity. If we decided to take this fallthrough semantics out of this RFC, then we're stuck with that behavior.

Using boolean is an ok example, but it doesn't strongly argue for it as opposed to some other example I posted earlier wrt animal and one that @vegorov-rbx posted with pcall example. If a design point is a "choose now or forever hold your peace," it especially needs an equally as compelling argument to support it.

So I'd rather block the RFC until we are able to agree on this design point, whether locals should fallthrough or not.

An example where the behavior is breaking change would also be good to include in the alternatives section.

if local x = f() in x % 2 == 0 then
  print(x) --> any even numbers
else
  print(x) --> nil
end

As is, this RFC proposes the print(x) in the else branch to print any odd numbers.

end
```

`Output: true truthy block`

In the latter case, the `exp` condition is checked rather than the initializer.

Example:

```lua
local function foo()
return true
end

if local b = foo() in b == false then
print(b, "truthy block")
else
print(b, "falsy block")
end
```

`Output: true falsy block`

When declaring multiple values inside of an initializer, the `in` clause is required.

Example:

```lua
local function foo()
return true, false
end

if local a, b = foo() in a and b then
else
print'Hello World, from Luau!'
end
```

`Output: Hello World, from Luau!`
Comment on lines +106 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an eye towards consistent behavior with destructuring in the future, I would prefer to see this changed to requiring a and b be non-nil implicitly, but it's ultimately okay if we air on the conservative side here since this requirement is forwards-compatible with changing it in the future to mean that. In general, I think the behavior we want though is that using local a = foo() and local a, b = foo() in a condition will only ever take that branch when the results are non-nil.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. If you want to allow b to either exist or not, you should probably have to say that explicitly. Otherwise the fact that you asked for local a, b = foo() probably means that you wanted both a and b to both exist.


If statement initializers are also allowed in `elseif` conditions.

Example:

```lua
local a = false
local function foo()
local b = a
a = true
return b
end

if local a = foo() then
elseif local b = foo() then
print(b)
end
```

`Output: true`

# Drawbacks

Parser recovery may be more fragile due to the `local` keyword.

Initializers increase the complexity of the language syntax and may obscure control flow in complex conditions.

# Alternatives

A different keyword or token can be used in place of `in`.

Rather than use `in`, we could introduce a new contextual keyword, or use a different existing keyword like `do`.

```lua
if local a, b = foo() do b > a then
print'Hello World, from Luau!'
end
```

While Luau is a verbose language that uses keywords for the majority of its syntax, another approach is using semicolons as a separator. This can work well because statements can use semicolons as a separator, which will retain consistency with the language. The same can be said for the comma, which would be consistent with for loop syntax.

```lua
if local a, b = foo(); b > a then
print'Hello World, from Luau!'
end
```