wing icon indicating copy to clipboard operation
wing copied to clipboard

Dedicated casting syntax

Open Chriscbr opened this issue 1 year ago • 14 comments

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:

  • x as cloud.Bucket (note: conflicts with existing "as" syntax for preflight class instantiation https://www.winglang.io/docs/language-reference#33-preflight-classes)
  • x -> cloud.Bucket
  • x asType cloud.Bucket
  • @cloud.Bucket x
  • <cloud.Bucket>x
  • (cloud.Bucket)x
  • [cloud.Bucket]x
  • cast<cloud.Bucket>(x)

Component

Language Design, Compiler

Community Notes

  • Please vote by adding a 👍 reaction to the issue to help us prioritize.
  • If you are interested to work on this issue, please leave a comment.
  • If this issue is labeled needs-discussion, it means the spec has not been finalized yet. Please reach out on the #dev channel in the Wing Slack.

Chriscbr avatar Oct 10 '23 15:10 Chriscbr

This is already done no?

eladb avatar Oct 12 '23 13:10 eladb

@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.

Chriscbr avatar Oct 12 '23 13:10 Chriscbr

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`

eladb avatar Oct 12 '23 17:10 eladb

@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?

Chriscbr avatar Oct 12 '23 17:10 Chriscbr

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.

eladb avatar Oct 12 '23 20:10 eladb

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?

Chriscbr avatar Oct 13 '23 21:10 Chriscbr

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

eladb avatar Oct 14 '23 14:10 eladb

@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

Chriscbr avatar Oct 18 '23 17:10 Chriscbr

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

eladb avatar Oct 18 '23 18:10 eladb

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] avatar Dec 18 '23 06:12 github-actions[bot]

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] avatar Feb 17 '24 06:02 github-actions[bot]

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.

thoroc avatar May 13 '24 06:05 thoroc

@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)

eladb avatar May 13 '24 06:05 eladb

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

Chriscbr avatar May 13 '24 14:05 Chriscbr