wing
wing copied to clipboard
Revisit `as "id"` syntax / "as" keyword is confusing
We want to use as for casting, so let's revisit the way we specify an identifier for preflight objects.
One option is to replace the current usage with is:
new Bucket() is "myBucket";
new Bucket() is "yourBucket";
Another alternative is to add a synthetic keyword optional parameter called id to every preflight class initializer and plug it in:
new cloud.Bucket(); // defaults to "cloud.Bucket"
new cloud.Bucket(id: "my-bucket");
See below for some discussion.
Thoughts?
I kind of prefer the old fashioned way:
new Bucket(id: "yourBucket");
But I understand it leads to verbose and even possibly confusing paths...
So, is is fine to me.
Sounds good to me, I think unlocking "as" for casting would be nice.
What's the issue # for lang support for casting? Can help putting this in context.
I like new R() as "some_R" because it conveys the idea that there's a default and we're changing it.
I like new R() id "some_R" because it's explicit about what we're doing, setting a unique id for the resource.
I like new R() named "some_R" because it's very clear what we're doing, creating a resource and giving it a name.
is feels a bit off.
So not strongly opinionated.
Considering @skyrpex's idea. We can generate a synthetic optional keyword argument called id and then the usage will be:
new R(id: "some_R")
It's a bit of a hack, but from a DX perspective it feels right.
What would this mean for struct expansion in function calls with an id field? Wouldn't this clash with the named arguments?
Why not use to for casting instead of as?
What would this mean for struct expansion in function calls with an id field? Wouldn't this clash with the named arguments?
Yes, it would clash. We can decide one of the following:
- To make
ida reserved name for keyword arguments (could be a bit annoying) - If there's an explicit
id, just use this value as the logical ID. Basically "embrace the clash". - Use something like
$idor some other more unique name for the synthetic identifier.
At any rate, I am growing to like this direction because it does result in a very intuitive DX.
new cloud.Bucket(id: "my-bucket");
new cloud.Queue();
new cloud.Function(id: "my-function", inflight () => {
log("hey!");
});
I really hated the fact that the identifier was something you'd have to specify outside of the call.
Why not use
tofor casting instead ofas?
as is commonly used for casting and I don't think we have a strong justification to shift the mental model here.
Makes sense! Then I would go for embrace the clash. Seems like the best DX for when it clashes.
I like
new R() id "some_R"because it's explicit about what we're doing, setting a unique id for the resource.
I think the most practical way would be to replace the current as with id, as suggested by Yoav's syntax.
I personally find both id and is quite odd, since I'd expect an adverb in that position.
My proposals:
new R() alias "some_R"
new R() withId "some_R"
new R() withName "some_R"
Considering @skyrpex's idea. We can generate a synthetic optional keyword argument called
idand then the usage will be:new R(id: "some_R")It's a bit of a hack, but from a DX perspective it feels right.
Does the id always have to be the first argument?
Does the
idalways have to be the first argument?
No, it's a faux named keyword argument.
When is the id named argument available? Always? I guess we don't only want to have it when the last parameter is a struct, but also allow it when the last argument is an optional or variadic?
Good point @lancear, it should be supported for in all initializer signatures.
From an implementation perspective, the compiler will not pass it through as a keyword argument to the function but will pass it as the 2nd construct argument.
So:
new cloud.Bucket(id: "bang", encrypted: true);
Will emit the following JavaScript:
new cloud.Bucket(this, "bang", { encrypted: true });
As I said, it's a hack.
@yoav-steinberg curious what you think about this direction?
new cloud.Function(id: "my-function", inflight () => {
log("hey!");
});
The parser right now expects all positional arguments to come before named arguments. To support usage like the one above, would we have to model this basically as a special case?
Can the "id" be added anywhere in the parameter list? ie
new cloud.Service(
inflight () => { log("started!") },
id: "my-service",
inflight () => { log("done!"); },
port: 8080,
);
The parser right now expects all positional arguments to come before named arguments.
Really? What's the motivation behind this restriction?
The parser right now expects all positional arguments to come before named arguments.
I think this is great for readability, named arguments in between positional arguments can get quite difficult to read. Maybe we could ease the constraint to allow them either before or after all positional arguments, or only allow the named id argument also as the first argument.
I can see the argument for readability but maybe that's not something the language should be strict about, and leave it to the developer to decide, no?
When reading a function call, I generally expect the arguments to match the order that the function is expecting. Since struct expansion is just a sugar for struct construction as the last argument, seeing struct expansion tells me that I'm looking at the last argument.
Also worth noting we probably shouldn't call it "named arguments", as that confuses it with a similar concept in other languages that we don't support.
Enforcing id can only be the first argument (if it's given) could be a reasonable compromise:
// Error: Expected "id" to be the first argument
new cloud.Function(inflight (x) => { ... }, id: "my-function",);
// Error: Expected "inflight (str): str" but got "str". Did you mean to write `id: "my-function"`?
new cloud.Function("my-function", inflight (x) => { ... });
// OK
new cloud.Function(id: "my-function", inflight (x) => { ... });
that would also make it clear it's not a normal struct expansion argument.
btw is there any specific reason why we don't simply use the variable name of the resource as the id? Like that is the natural way to distinguish between instances of the same type in code already. 🤔
let myBucket = new cloud.Bucket();
new cloud.Queue(); // unnamed queue
let myFunction = new cloud.Function(inflight () => {
log("hey!");
});
is there any specific reason why we don't simply use the variable name of the resource as the id
Variables are too volatile to serve as identifiers. These identifiers are mapped to physical resource names, and we don't want a simple rename of an identifier to have an impact on the application's infrastructure.
I am still not convinced that we need to be too strict about where keyword arguments are positioned in the function call. Requiring that id: is a special thing that must only be the first argument feels like a much higher cognitive load from a API consumer standpoint. From the consumer's perspective id: could just just be treated as another optional keyword argument.
I understand the stylistic argument (but that's not something we have to enforce), but I am not sure the fact that the struct is declared as the last argument justifies this strictness.
So I personally am not strongly opinionated here but I do dislike this id argument direction, I kind of like the as "id" syntax as is.
Addressing some earlier discussions in the thread about not using as and maybe using named, alias, etc.. If this is for the sake of leaving as for casting I guess I see the point, however as would already be overloaded for not just casting but also aliasing.
bring "whatever" as lib;
So cognitively I dont think its too crazy to us new cloud.Bucket() as "my_bucket"
That said Id also be in favor of just changing new cloud.Bucket() as "my_bucket"
to some thing along the lines of new cloud.Bucket() named "my_bucket"
I've been looking for an alternative for the "as ID" syntax not only to free up the "as" keyword but also because it's a cumbersome syntax which ends up appearing a lot in the language. The id: idea turns this into something much less messy in my opinion, which is why I am excited to explore it.
This is ugly from an aesthetic/stylistic point of view:
new cloud.Function(inflight () => {
log("function 2");
}) as "f1";
new cloud.Function(inflight () => {
log("function 2");
}) as "f2";
This is much better:
new cloud.Function(id: "f1", inflight () => {
log("function 2");
});
new cloud.Function(id: "f2", inflight () => {
log("function 2");
});
I also think it will reduce the cognitive load because it's one less concept of understand when you are creating objects.
This is ugly from an aesthetic/stylistic point of view:
Yea but maybe its like a pug or chihuahua, soo ugly its cute 😁
Maybe.
A bit torn between as (or similar named keywords) and something like a virtual argument. While I found the as "myid" syntax odd in the beginning, it's nicely conveying that it's not a functional aspect of the construct, but something which serves a special purpose. Also, it's easy to add / remove on existing constructs.
On the other hand, having actual arguments probably fits better in the mental model of how to create and name constructs (in particular people coming from a cdk background).
Speaking of cdk, how would this work for cdk dependencies (e.g. resources from the @cdktf/aws-provider).