-
Notifications
You must be signed in to change notification settings - Fork 52
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: Function default arguments #91
base: master
Are you sure you want to change the base?
RFC: Function default arguments #91
Conversation
f2edf59
to
8d84199
Compare
8d84199
to
a693fcf
Compare
### Language implementation | ||
Within the AST, it would likely be simpler to add a new `AstArray<AstExpr*> argsDefaults` to `AstExprFunction` alongside `args`, rather than modify `args` to be an `std::pair` due to the existing widespread usage of `args`. | ||
|
||
Rather than implement this as a feature of the `CALL` instruction within Luau's VM it is instead suggested by this RFC to implement this as part of the compiler. This both increases compatibility (as the VM remains unchanged) and makes it easier to allow *any* expression to be used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does this mean function f(x: number = 3)
means that I call f()
and it compiles as f(3)
?
What happens then if I use that function as a first class value?
What happens then in this case?
local function g(callback: (number) -> ())
callback()
end
g(f)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ask because surely something with default parameters cannot have a non-optional argument as part of its type and also be a first class value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The suggested implementation essentially injects a series of if x == nil then x = default end
statements at the start of the function body, so passing f
around as a first class value is totally fine. If we run
-- !strict
function f(x: number = 3)
print(x)
end
local function g(callback: (number) -> ())
callback()
end
g(f)
we get 3
on stdout as expected.
We do however get a type error of TypeError: Argument count mismatch. Function 'callback' expects 1 argument, but none are specified
because the first class value f
will have a type of (number?) -> ...any
as defined there (to indicate that we need not provide that argument/could provide nil
).
Amending g to local function g(callback: (number?) -> ())
that type error goes away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a section that hopefully formalises that brief explanation into the RFC. lmk if there's still confusion (or another type interaction I've not formalised!).
What is the behavior of the upvalue references in the default value expressions? Do they act like any other upvalue reference from within the body? For example: local x = "foo"
local function getX(arg = x)
return arg
end
print(getX()) --> foo
x = "bar"
print(getX()) --> foo or bar? |
It would be |
If the intent is for this to replace redefining the argument with a default value in the body then it would make sense for it to be captured as an upvalue |
How does this interact with forwarding calls? function f(x = 0, y = 0) return x + y end
function g(...) return f(...) end
print(g()) |
I don't like default values for function arguments; I think they cause more problems than they solve. |
I think any questions of the logic are resolved by viewing the default argument as a reassignment.
would be equivalent to
|
As described in the document at the moment, the semantics mean On the other hand, this means passing I'm genuinely unsure which would be better (and maybe JavaScript was cooking with |
I believe that the implementation could benefit from the use of named arguments like in other languages such as python, where def mix(alpha, low=0, high=1):
return low + (high - low) * alpha
mix(0.5) # called with default arguments
mix(0.5, -1.0) # default argument for (high) used
mix(0.5, high=20.0) # called with named parameter, low is still default
mix(0.5, high=2.0, low=-2.0) # order of named parameters is irrelevant
mix(0.5, None) # 'None' is used in place of low, as opposed to '0', results in error
# illegal function definition, results in an error as default arguments cannot be followed by normal arguments
def mix(alpha, low=0, high):
pass Luckily, Luau does not make use of assignment operators as arithmetic operators, so there is no ambiguity where grammar is unsolvable between I am also under the assumption that the default value should be passed as an expression. This would resolve any issues that would arise if developers modify the value, and it would allow for operations to be computed within the default argument expression, such as the following: local function heapInsert(heap, element, limit=MAX_SIZE-1)
-- insert item into heap
end
-- later, when developer is adjusting their constants
Heap.MAX_SIZE = 2048; However, one caveat with default values is that they are not always a good idea depending on the implementation! local function deep_search(tbl, item, ignore={})
-- initial check for custom ignore table
if ignore[tbl] then return nil, nil end;
-- add to ignore list to prevent cyclic access
ignore[tbl] = true;
-- initialize queue
local queue = {tbl};
-- initialize temporary variable for current table
local temp;
-- while queue is not empty;
while #queue > 0 do
-- get table from queue
temp = table.remove(queue, 1);
-- iterate through table
for k, v in temp do
-- if item has been found, return the key and table it was found in
if v == item then return k, temp end;
-- if item is a table and is not in ignore list, add to the ignore list and push into queue
if type(v) == "table" and not ignore[v] then
ignore[v] = true;
table.insert(queue, v);
end
end
end
return nil, nil;
end If the implementation evaluates This can be observed in the following example: local key, found_table;
local deep_table = {
{1, 2};
{3, 4};
};
deep_table.self = deep_table;
key, found_table = deep_search(deep_table, 3); -- should return '1', and a reference to '{3, 4}'
key, found_table = deep_search(deep_table, 3); -- would return nothing, as 'ignore' would hold a reference to deep_table from the previous iteration In some languages, this would be the case, so it would greatly depend on the design of the default parameter as to whether or not this would be applicable, however the disadvantage of this system means that you must define constants as locals for performance, as otherwise table constructors and other expressions would result in re-evaluation for every instance of a function call when not overwriting the default argument: local someConst = table.create(1_000, 0.0);
local function test(arg1, arg2 = someConst)
-- ...
end
-- calls 'table.create(1_000, 0.0)' whenever test2() is called without 'arg2'
local function test2(arg1, arg2 = table.create(1_000, 0.0))
-- ...
end There is also the potential to expose existing parameters to default parameters, but this may increase code complexity significantly local function some_math(a, b, c=(a*b))
-- ...
end
-- as opposed to using (high=-1) and computing (#array+high+1)
local function search(array, low=1, high=#array)
-- body
end |
Rendered
This RFC proposes adding default argument values, removing the need for
if arg == nil then arg = default end
cases at the start of every function.TL;DR
along with all of the semantics and type inference you might expect from that example.
I have a working implementation of this feature on my luau fork. The brave adventurer may wish to run a build and play around but otherwise this serves more to show that this could be reasonably add into the language with minimal work required.