winglang / wing

A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡
https://winglang.io
Other
4.73k stars 185 forks source link

Dedicated casting syntax #4498

Open Chriscbr opened 8 months ago

Chriscbr commented 8 months ago

Feature Spec

In Wing 0.x, the unsafeCast builtin function has been replaced with a dedicated syntax.

Use Cases

Today unsafeCast doesn't let you specify the type you are casting to because it is just a blanket function that lets you to treat a value as type anything. A casting syntax can address this. Most other modern languages also support casting through a dedicated syntax.

Implementation Notes

Random syntax ideas:

Component

Language Design, Compiler

Community Notes

eladb commented 8 months ago

This is already done no?

Chriscbr commented 8 months ago

@eladb We added unsafeCast function, but I'm not sure it's the right DX since it leaks the any type without a strong reason. A dedicated syntax like (cloud.Bucket)x would require you to specify what type you are casting to. I've suggested some ideas in the issue.

eladb commented 8 months ago

I am not sure I completely understand... Isn't unsafeCast() inferring the target type based on what we are assigning to?

let x: Foo = unsafeCast(bar); // cast to `Foo`
let y: Zing = unsafeCast(bar); // cast to `Zing`
Chriscbr commented 8 months ago

@eladb Sorry my explanation wasn't clear. Here's an example of what I mean:

let b = new cloud.Bucket();
let z = unsafeCast(b);

In this example, z is now type any (that's what it shows when you hover over the variable in VS Code). This means it can be passed to any functions, and any properties or methods that don't exist on it can accessed. To date the team has put in a lot of effort into keeping Wing type-safe per the language's goals, including avoiding exposing any types.

Do you have use cases in mind where you need the capability to cast something as any?

eladb commented 8 months ago

I see. Can we forbid this particular situation and require that unsafeCast will always be used in the context of an explicitly defined type? I understand there's some "reverse type inference" thing that needs to happen but maybe if it's not too hard to implement, we don't need the dedicated syntax.

Chriscbr commented 8 months ago

Can we forbid this particular situation and require that unsafeCast will always be used in the context of an explicitly defined type? I understand there's some "reverse type inference" thing that needs to happen...

I'm not sure if allowing the target type to be inferred from context feels right - here's an example of what I'm worried about:

Let's say a user wants to cast an IConstruct into a cloud.Bucket so they can pass it to a class constructor from a third party library. A user has written this code so far:

bring "file-copier" as copier;

let service = new MyService();
let bucket = service.tryFindChild("Storage");
new copier.FileCopier(bucket); // error: expected Bucket, but received IConstruct

They discover unsafeCast lets you cast between types, so they fix it like so:

// snip
new copier.FileCopier(unsafeCast(bucket));

unsafeCast was used in the context of FileCopier which explicitly expects a Bucket, so it was cast to a Bucket and the code compiles now. Cool 😎

A few weeks later, the user upgraded their dependency, compile their project, deploy it, and the file copier stops working, but they can't tell why.

It turns out, FileCopier had a breaking change, and it now accepts a Redis instead of a Bucket. But the user didn't have to specify the type they're casting to, so their code continued to compile.

If they were required to specify a target type, this bug wouldn't have been possible since the error would have be caught at compile time:

new copier.FileCopier(unsafeCast<cloud.Bucket>(bucket)); // error: expected Redis, but received cloud.Bucket

WDYT?

eladb commented 8 months ago

Thanks for the example, I can now see the importance of explicitly specifying the casting target.

So now we need to decide what's the preferred syntax. @Chriscbr curious if you have a preference.

I am wondering if we should reconsider the as ID syntax? It's quite ugly and it's not uncommon to use it, so maybe we can revisit that and use as Type for casting...

Perhaps a string between the type name and the () in a new expression would work better for the id:

new cloud.Bucket "dear-bucket" ();

It's a quirky but it has a kind of feels like the id is the first implicit argument of the constructor...

Chriscbr commented 8 months ago

@eladb yeah, I'm totally open to alternatives to as ID - definitely something worth playing around with.

Do we use "#" for anything? I sometimes associate #name with IDs because of CSS, maybe there's something we could do with that...

Another idea I had was:

new cloud.Bucket() named "Storage";

Re: casting, I talked with @MarkMcCulloh and other folks on the compiler team, and it was mentioned it could be OK to use the generic type param syntax unsafeCast<T>(value) or cast<T>(value) as an incremental solution

eladb commented 8 months ago

Not a huge fan of this syntax but I am okay to try it n

github-actions[bot] commented 6 months ago

Hi,

This issue hasn't seen activity in 60 days. Therefore, we are marking this issue as stale for now. It will be closed after 7 days. Feel free to re-open this issue when there's an update or relevant information to be added. Thanks!

github-actions[bot] commented 4 months ago

Hi,

This issue hasn't seen activity in 60 days. Therefore, we are marking this issue as stale for now. It will be closed after 7 days. Feel free to re-open this issue when there's an update or relevant information to be added. Thanks!

thoroc commented 1 month ago

Personal preference would be to something that is visibly clearly showing the casting. So last option would be best: cast<T>(x) Although I have seen and like the specific approach for the base type to have their own cast Str(x), Bool(x) and so on.

eladb commented 1 month ago

@cast<T> works for me (the @ denotes that this is a builtin)

(and perhaps we can replace unsafeCast with @cast<any> which is practically what it is)

Chriscbr commented 1 month ago

I'm for changing it to @cast as long as the user's required to specify the type they're casting to. We'd need to add support in the compiler to parse <type> clauses in intrinsic function calls but it feels doable.

I think the ergonomics as T is still pretty convenient, so it might also be worth considering as a shorthand. As long as casting in Wing has no runtime cost, I think it would lower cognitive load for new users if Wing adopts the syntax already used by TypeScript and Rust. (I recognize cast being a function is common in some other languages like C++, but in terms of language adoption I think more folks adopting Wing are probably coming from TypeScript).