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: Function default arguments #91

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

Conversation

Bottersnike
Copy link

@Bottersnike Bottersnike commented Jan 14, 2025

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

function random_point()
    return { x=math.random(), y=math.random() }
end
function print_manhattan_distance(message_prefix = "", point = random_point(), origin = { x=0, y=0 })
    print(message_prefix .. math.abs(point.x - origin.x) + math.abs(point.y - origin.y))
end

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.

@Bottersnike Bottersnike force-pushed the function-default-arguments branch from f2edf59 to 8d84199 Compare January 14, 2025 02:48
@Bottersnike Bottersnike force-pushed the function-default-arguments branch from 8d84199 to a693fcf Compare January 14, 2025 03:01
### 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.
Copy link
Contributor

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)

Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Author

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!).

@stravant
Copy link

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?

@Bottersnike
Copy link
Author

It would be bar as the arguments are evaluated every time the function is called. Making this not be the case would require either dropping the behaviour where evaluations are performed at call time, or special-casing single upvalues, both of which I don't think would be ideal?

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.

3 participants