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 10 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
171 changes: 171 additions & 0 deletions docs/syntax-if-statements-initializers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 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() where 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' |
...

- ifelseexp = 'if' exp 'then' exp {'elseif' exp 'then' exp} 'else' exp
+ ifelseexp = 'if' cond 'then' exp {'elseif' cond 'then' exp} 'else' exp

TheGreatSageEqualToHeaven marked this conversation as resolved.
Show resolved Hide resolved
+ cond = 'local' binding '=' exp ['where' exp] |
+ 'local' bindinglist '=' explist 'where' 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() where 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 `where` clause is required.

Example:

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

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

`Output: Hello World, from Luau!`

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 `where`.

Introducing a new contextual keyword can introduce complexity in the parser, and a different existing keyword like `in` or `do` could be used instead.

```lua
if local a, b = foo() in 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
```