-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another thing: what happens if you use 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 commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# Export Keyword | ||
|
||
## Summary | ||
|
||
Allow users to export methods and values from their libraries through the use of the export keyword. | ||
|
||
## Motivation | ||
|
||
Users can export types from their libraries through the use of the export keyword. | ||
|
||
```luau | ||
export type Point = { x: number, y: number } | ||
``` | ||
|
||
Consumers of these libraries are able to access exported types by indexing the libraries identifier. | ||
|
||
```luau | ||
local Library = require("Library") | ||
local point : Library.Point = { x = 0, y = 0 } | ||
``` | ||
|
||
Right now, this is the only use of the export keyword but it would be great if we let users export more with it to offer more utility and make exporting library methods and values easier. | ||
|
||
## Design | ||
|
||
### Syntax | ||
You can prefix any non-local declaration with the export keyword in the top level scope of a module. For values this looks like: | ||
|
||
```luau | ||
export version = "5.1" | ||
``` | ||
|
||
While methods use: | ||
|
||
```luau | ||
export function init() | ||
-- do a thing | ||
end | ||
``` | ||
|
||
### Behavior | ||
|
||
When a value or method is prefixed with export, it is automatically added to a hidden export table which is frozen as soon as the module returns. | ||
|
||
Take the following module: | ||
|
||
```luau | ||
export type Point = { x: number, y: number } | ||
|
||
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 | ||
``` | ||
|
||
This essentially becomes sugar for: | ||
|
||
```luau | ||
local _EXT = {} | ||
|
||
type Point = { x: number, y: number } | ||
type _EXT.Point = Point -- note: this doesn't actually work today but one day it should | ||
|
||
local 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 | ||
_EXT.distance = distance | ||
|
||
return table.freeze(_EXT) | ||
``` | ||
|
||
If the user attempts to assign to an exported identifier then we would throw an error explaining that the interface cannot be changed once it has been exported. | ||
|
||
### Nuances | ||
Due to the implementation, most things should "just-work". Here are some examples to consider: | ||
|
||
#### Calling an Exported Function | ||
You can call an exported function as it's registered as a local before being added to the export table. | ||
|
||
```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 | ||
|
||
distance({0, 0}, {1, 1}) | ||
``` | ||
|
||
#### Nested Tables | ||
You can export tables with additional values inside of them. | ||
|
||
```luau | ||
export triangle = {} | ||
|
||
function triangle.draw() | ||
|
||
end | ||
``` | ||
|
||
Something important to note here is that the nested table, `triangle` is not frozen. | ||
|
||
#### 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 commentThe reason will be displayed to describe this comment to others. Learn more.
What kind of error? Parse error? Compile-time error? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like it should be a parse error similar to
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
```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 | ||
} | ||
``` | ||
Comment on lines
+104
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps worth mentioning that the future change that would enable There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
## Drawbacks | ||
The keyword already exists for types so there's not much cost in us adding support to values. The main drawback is that it's another way to do module exports. We already have a way to do that, do we really need another? | ||
|
||
## 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 commentThe 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 With There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I don't want to expose There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. |
||
|
||
### Automatically Export Globals | ||
Luau has a separate global scope for each module rather than a shared global scope across the entire program. We could automatically export any values that are stored in the global scope. This isn't backwards compatible though, we'd likely need a new import mechanism to resolve 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.
One thing I noticed not mentioned here is how this would work with two mutually dependent exported functions. If you replace
export
withlocal
in this snippet, it doesn't do what you want it to do.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.