Skip to content

What's the right syntax for an implicit cast/inline cast operator? #193

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

Open
leafpetersen opened this issue Jan 23, 2019 · 10 comments
Open

Comments

@leafpetersen
Copy link
Member

leafpetersen commented Jan 23, 2019

There is a longstanding desire to have better syntax for casts that appear inline in a method chain:

  ((a as Whatsit).frobIt(3) as Whosit).fizzle();

If we move ahead with #192 there is also a desire to have a short syntax for implicitly casting to the context type (see for example the hand-rolled cast function used in this cl removing implicit casts).

It seems likely that we can solve both of these with a single piece of syntax. For example, we could add a postfix cast operator !! which casts to the context type, also usable in the form !!<T> which casts to T. Code that formerly used an implicit cast:

takeList(List<int> f) {}
Iterable<int> mkIterable() {}
void test() {
  takeList(mkIterable());
}

could be rewritten as follows:

takeList(List<int> f) {}
Iterable<int> mkIterable() {}
void test() {
  takeList(mkIterable()!!);
}

And the example from above with inline casts could be written more cleanly as well:

  a!!<Whatsit>.frobIt(3)!!<Whosit>.fizzle();

We might wish to specify it to be an error to use !! to perform a side cast when the type is not explicitly written.

An alternative syntax which would be more consistent with existing Dart code would be to use .as and .as<T>.

takeList(List<int> f) {}
Iterable<int> mkIterable() {}
void test() {
  takeList(mkIterable().as);
}
  a.as<Whatsit>.frobIt(3).as<Whosit>.fizzle();

Unfortunately this would be a breaking change, since as is not a reserved word in Dart. In practice this is likely non-breaking, @munificent is proposing to scrape some code to verify this.

If we had extension methods + generic getters, we could specify this as a generic extension method:

extension Cast on Object {
  T get as<T> => this as T;
}

This doesn't capture a side cast restriction, but it may avoid the breaking change issue. It seems unlikely that we will have both of these features in place in a suitable time frame to use this encoding though.

Other ideas for syntax that have been put out for consideration:

  • x.(as T) and something like x.(as) or x.(as _)
  • this could possibly be a use of two general mechanisms:
    • a way of using an operator in a postfix position
      • enabling await a -> a.await, a + b -> a.+(b), possibly also foo(x, y) -> x.foo(y)
        - a prefix version of our existing cast operator
  • x.cast<T>
    • This is probably not feasible, since we have cast methods on various core library types.
@Hixie
Copy link

Hixie commented Jan 23, 2019

  a!!<Whatsit>.frobIt(3)!!<Whosit>.fizzle();

That looks completely impenetrable to me. I can't imagine showing new users this syntax. At least ((a as Whatsit).frobIt(3) as Whosit).fizzle(); is clear about what it does, even if it is a bit faffy.

@Hixie
Copy link

Hixie commented Jan 23, 2019

I like the idea of adding an as method (or getter) to Object that does the cast, that looks much better.

@munificent
Copy link
Member

I'm with @Hixie. We already have a two-letter token that users understand means "cast" -> as. It would be a shame to not take advantage of it. ! is particularly problematic because:

  • We are already telling users "!" means "logical not" and before long we'll also tell them it means "assert not null", so using it to mean a third thing seems confusing.

  • Adding a new postfix operator with unclear precedence causes confusion when mixed with other operators. Do we expect users to know what these do:

    !foo!!
    ++i!!
    await foo!!;
    foo ? bar : bang!!
  • It's not clear how this should be formatted when in the middle of a method chain that spans multiple lines.

@munificent
Copy link
Member

I did a little poking around to see how breaking it would be to add a .as syntax.

Inside google, I see only a couple of uses of as as an identifier (query ".as\b case:yes lang:dart"). Most are in Dart SDK tests. The one real use is in the code_builder package, which is maintained by us. It doesn't appear to be accessed outside of that package.

I also pulled down the most recent versions of all 5,176 packages on pub. In 74,376 parseable files, I found 415 'as' identifiers out of 17,728,837 total identifiers (0.00234%).

Those are scattered across 53 different packages. 8 of those packages start with "m4d_" and 9 start with "angel_". That's a little more than I expected, but possibly within the range of things we could conceivably cope with.

@lrhn
Copy link
Member

lrhn commented Jan 24, 2019

We have the option of doing nothing, and still disallow implicit downcasts, which would mean that all casts would require the as type syntax.

I would like to have a good cast operator that works well with chaining. (I also want a suffix await).
Adding a shorter, stronger associating syntax will make the migration form implicit to explicit downcast easier.

I don't particularly like int x = foo.as;. I just think it reads badly, as without a subject does not say "cast" to me. If we could do foo.cast then I would be happier reading it (but likely not writing it), but that word is definitely used.

If we introduce suffix ! as "cast from nullable to non-nullable" (a down-cast), I originally wanted that to work for any down-cast. I was convinced that it would be likely to hide errors, which is why we are looking for a second cast operator to handle non-null based downcasts, if implicit down-casts are not supported. Using !! has the advantage of being a small syntactic increment over the other down-cast operator, !. Using a significantly different name feels more inconsistent than not using as does.

I'm not worried about the suffix operator precedence. There is nothing new about that, we already have suffix operators (the increment and decrement operators, the index operator).

!foo[1]
++i[1]
await foo[1];
foo ? bar : bang[1]

Neither of these are hard to read because we have learned that [..] binds strongly (and so does .foo).
I'm sure you can write something unreadable, you already can, so the question is whether such things will occur organically, and how often.

For example:

  • !foo!! can occur if you were doing an implicit downcast of foo from Object to bool. I don't think that will happen often, though.
  • ++i!! is a compile-time error, since i!! is not an l-value.
  • await foo!! makes sense if you were doing an implicit down-cast of foo from Object to Future<?>. Again, not that common.
  • foo ? bar : bang!! should probably bind the !! to the bang (like we currently bind a trailing ++ to the bang). Not sure it will work, because we do don't propagate the context type context into the branches, so casting to context-type won't help you.

So, all in all, I don't think the examples are representative of actual code, and any problem with them should also apply to the single suffix ! operator.

@munificent
Copy link
Member

(I also want a suffix await).

Me too. Leaf and I spent a little time noodling on a more uniform syntax that would cover postfix as, await and possibly other infix operators but nothing really gelled.

I don't particularly like int x = foo.as;.

I don't either, but that's also not a representative example. Idiomatic code would be:

var x = foo as int;

I'm also not super crazy about:

functionThatExpectsInt(foo as);

But, then, I would be OK with not having a "cast to expected type" operator at all, so this may just be a specific case of that general feeling. :) I think this looks even worse:

functionThatExpectsInt(foo!!);

There is nothing new about that, we already have suffix operators (the increment and decrement operators, the index operator).

The difference is that most of the other operators we have have sixty years of programming history and millions of users behind them already. The times when we have added new operators (..), the precedence has been a problem. We had to tell people and then force dartfmt to always insert line breaks to steer around the profound confusion of:

point..x = 1..y = 2;

Worse:

true ? a : b..add("added")

I had to try this out because even I didn't know what this did and I work on the language. (Answer: it adds to a, not b.)

So, I do think we should be very careful around new operators and precedence.

I'm somewhat worried about the single suffix ! too, for that matter. But there is at least some prior art for that in Kotlin and Swift.

@Hixie
Copy link

Hixie commented Jan 26, 2019

Yeah personally I am not a fan of suffix-! and would rather we didn't add it either. But that's a topic for another bug, probably.

@pschiffmann
Copy link

If the goal is to get rid of the parens, I'd like to suggest a similar idea as I did in issue #25: Use the pipeline operator from #43.

(a as Whatsit)
  |. frobIt(3)
  |> (as Whosit)
  |. fizzle();

There's one difference to my suggestion on #25. The await keyword would have to be special-cased, because it can't be desugared into a synchronous function. However, (as Whosit) is just syntactic sugar for (o) => o as Whosit.


If the problem is that the target type of a cast is too long to write out, could it be an option to introduce a special identifier or keyword that denotes the context type of the current expression? For example, if we chose $ for this purpose, we could write explicit downcasts as:

int extractId(Map<String, dynamic> json) => int.parse(json['id'] as $);

And maybe even use it here?

List<num> nums = [1, 2, 3];
List<int> ints = [...nums.cast<$>()];

@leafpetersen
Copy link
Member Author

@leafpetersen
Copy link
Member Author

An inline cast operator can be considered separately. For implicit downcasts only, possibilities would be:

a!!
a as _
a as <>

The later potentially generalizes also a as <type> to mean downcast to type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants