-
Notifications
You must be signed in to change notification settings - Fork 39
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: Export Keyword for Values #42
base: master
Are you sure you want to change the base?
Conversation
Today, you can use the export keyword along with a return statement at the end of your module. If you use the export keyword with a value however we will throw an error if you also attempt to return. | ||
|
||
```luau | ||
export function distance(a: Point, b: Point) | ||
local x, y = a.X - b.X, a.Y - b.Y | ||
return math.sqrt(x * x + y * y) | ||
end | ||
|
||
return table.freeze { | ||
distance = distance | ||
} | ||
``` |
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.
Perhaps worth mentioning that the future change that would enable
type _EXT.Point = Point -- note: this doesn't actually work today but one day it should
would also likely necessitate this restriction on explicit returning as well, since the way we'd likely be able to make that possible is by giving tables the ability to have (phantom, i.e. non-existent at runtime) types associated with them as a 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.
To me that implies we need to make this functional (even if it's bad). We can't silently drop the second return, and we can't let it override the returned table. If modules supported multiple returns we might be able to get around this by making sure the export table is always the last value returned (e.g. return <user-defined>, _EXT
), not sure how others would feel about this.
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.
Sorry, maybe I wasn't clear. I think the correct behavior here, as you're suggesting in the RFC, is that we should throw an error on any explicit return in a module with export function
or export id
, and I also think that even though this may seem a bit odd right now since the restriction is specific to exporting values, it's also a restriction we will have for types if we do the work to enable assigning a type into a "field" (not one that exists at runtime) as you had written in the earlier example.
## Alternatives | ||
|
||
### Do Nothing | ||
It's already possible to export values from modules using a return statement. We don't actually need to do this, it's more of a nice-to-have. |
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.
Note that it's not entirely syntactic sugar. There are some subtle but useful runtime semantics we can squeeze out.
local mod = {}
function mod.distance(p1: Point, p2: Point)
local x, y = p1.X - p2.X, p1.Y - p2.Y
return math.sqrt(x * x + y * y)
end
If you wanted to call mod.distance
somewhere else in this module, it will not get inlined because in theory, mod.distance
could have been transmuted to a different function with a different implementation.
With export function distance
, it's equivalent to local function distance
+ exports.distance = distance
.
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.
This is true, and also worth calling out. There's an additional question here around whether we should expose _EXT
to users at all. It might be worthwhile if there are legitimate use-cases with metatables but otherwise we might want to make it innaccessible.
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.
Yeah I don't want to expose _EXT
. It's trivial for the VM to not expose it too, it already does this with loops which introduces 3 locals you can't even use.
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 also question the value of adding metatable to the export table. I've never seen anyone want to do this, and if they do they can use return
with no export
s.
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.
Yeah, in my mind, the initial goal should be to support the primary way people write modules today well, and other use cases can clearly continue to use explicit return. We can always consider adding additional things (like exposing that internal table as an identifier) in the future if we see sufficient motivation for doing so, but we cannot ever take it away if we add it now.
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.
Agree with this so let's keep it locked down and if legitimate use-cases come up we can discuss as part of another RFC.
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.
One thing I noticed not mentioned here is how this would work with two mutually dependent exported functions. If you replace export
with local
in this snippet, it doesn't do what you want it to do.
export function f()
g()
end
export function g()
f()
end
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 think my personal expectation here is that this is going to behave the same way as locals, and we can perhaps provide a plain export f
export g
(though I suspect syntactic ambiguity will be an issue here) to let you export a global.
I would be open to hearing reasons why it should go the other way instead and behave like globals, but I think regardless of the choice, we don't want to introduce a new third semantics here, export
should behave like one of them.
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.
Agree with @aatxe on this one.
If we want to support the described behavior I think we should look into hoisting but that would be a separate RFC.
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.
Given that export
probably only makes sense at module level, it stands to reason they should work like globals.
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.
Another thing: what happens if you use export
in a scope that isn't the module scope?
function set(x)
export function f() return x end -- ????
return f
end
-- ????
print(set(5)())
print(set("hello")())
We have this problem already, but it's isolated to the type system currently which is easy to break compatibility, but this is runtime stuff.
function f()
export type Foo = number
end
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 actually been wondering how this should work. My feeling is that we either don't allow it or we make it work exactly how it does in the modules function scope. In other words, using export
in any function scope creates a module table and returns it as that functions return value. It would be a weird language quirk so I'm leaning towards "it's not allowed" but maybe that's done through linting and not something we actually enforce.
Something important to note here is that the nested table, `triangle` is not frozen. | ||
|
||
#### Multiple Returns | ||
Today, you can use the export keyword along with a return statement at the end of your module. If you use the export keyword with a value however we will throw an error if you also attempt to return. |
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.
Throw an error
What kind of error? Parse error? Compile-time error?
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'd imagine compile-time.
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 thought so too, but wanted this to be specified in the RFC.
Also, note that compile-time errors are not shown to the user at edit time currently, and has a problem of only throwing one compiler error. We'll need a linter to warn on mixed use of export
/return
.
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.
Seems like it should be a parse error similar to
return "Foo"
return "Bar"
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.
Yeah, I suppose it's not hard to make it a parsing error, you just keep a little bit of state to record if you've already seen an export
/return
and then whichever you see second can produce an error about it.
Co-authored-by: Alexander McCord <[email protected]>
I think this is a great UX improvement! I do have one issue with it: When you require a module with no return in Roblox, it gives the error: "Module code did not return exactly one value." Having to specify a return by default and then remove it once you add something you want to export would be a bit annoying. I'm not sure what the right solution would be. Is it possible to change the behavior of modules that don't return anything? |
I think it would be reasonable to have an implicit |
Not sure I agree with |
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 very much would like this feature in Luau for many reasons (expecially easier AST analysis and standardization), but because it's a syntax change I don't think it meets the bar to be added as a syntax feature.
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.
With further introspection I'm leaning fully on the side of not accepting this RFC. While nice in theory the practical gains of the feature are marginal and (in my opinion) don't justifu the increased lexer/parser complexity.
As someone who has written a luau parser in luau, and contributed to other luau parsers, the added parsing complexity is just about 0. |
This is already what the behavior is (leaving nothing on the stack) though, and what's why you get an error saying it has to return exactly one value. Returning nothing is returning zero values! There's a question of whether |
There's an opportunity to talk about the possibility of adding re-exports here that I think this misses: export M = require("./foo") Which can re-export both types and values. |
Yeah I don't know why I just said that. Adding the export keyword would only require adding a new lex token type and having it generate a My point was is that I don't think I don't think this RFC meets the high bar of usefulness that's required for new syntax features. I was originally for approving this RFC but now due to further introsoection I'm againts the idea. |
Allow users to export methods and values from their libraries directly through the use of the export keyword.
Rendered View