-
Notifications
You must be signed in to change notification settings - Fork 205
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
Binding type cast and type check. #1191
Comments
Somewhat related (but you got to this first): #1201 |
I made an attempt to combine two nice properties: The idea from #1201 'if-variables' that we may introduce a variable with an existing name without repeating that name, and the idea from this proposal that the introduction of a new name should be usable in a general expression. The resulting proposal is #1210 'binding expressions'. |
@lrhn I would like to explore what some actual code looks like under these various proposals. Here's an example piece of code that I migrated, which I think suffers from the lack of field promotion. This is from the first patchset from this CL - I subsequently refactored it to use local variables, the result of which can be seen in the final patchset of that CL. This was a fairly irritating refactor, and I don't particularly like the result. Would you mind taking a crack at showing how this code would look under your proposal? // The type of `current` is `Node`, and the type of the `.left` and `.right` fields is `Node?`.
while (true) {
comp = _compare(current.key, key);
if (comp > 0) {
if (current.left == null) break;
comp = _compare(current.left!.key, key);
if (comp > 0) {
// Rotate right.
Node tmp = current.left!;
current.left = tmp.right;
tmp.right = current;
current = tmp;
if (current.left == null) break;
}
// Link right.
right.left = current;
right = current;
current = current.left!;
} else if (comp < 0) {
if (current.right == null) break;
comp = _compare(current.right!.key, key);
if (comp < 0) {
// Rotate left.
Node tmp = current.right!;
current.right = tmp.left;
tmp.left = current;
current = tmp;
if (current.right == null) break;
}
// Link left.
left.right = current;
left = current;
current = current.right!;
} else {
break;
}
} |
I'd rewrite that code as: // The type of `current` is `Node`, and the type of the `.left` and `.right` fields is `Node?`.
while (true) {
comp = _compare(current.key, key);
if (comp > 0) {
if (current.left is! Node currentLeft) break;
comp = _compare(currentLeft.key, key);
if (comp > 0) {
// Rotate right.
current.left = currentLeft.right;
currentLeft.right = current;
current = currentLeft;
if (currentLeft.left is! Node leftLeft) break;
currentLeft = leftLeft;
}
// Link right.
right.left = current;
right = current;
current = currentLeft;
} else if (comp < 0) {
if (current.right is! Node currentRight) break;
comp = _compare(currentRight.key, key);
if (comp < 0) {
// Rotate left.
current.right = currentRight.left;
currentRight.left = current;
current = currentRight;
if (currentRight.right is! Node rightRight) break;
currentRight = rightRight;
}
// Link left.
left.right = current;
left = current;
current = currentRight;
} else {
break;
}
} If writing it from scratch, I'd probably go with more positive tests and else branches for the breaks. (Also, consider making var currentLeft = current.left ?? break; The "problem" with |
To fill out the option space on promoting non-local variables, let's have a design sketch for a binding type cast and type check.
Dart has type check
e is int
/e is! int
and type caste as int
which both check the type ofe
againstint
. The check evaluates to abool
, and ife
is a local variablex
, it can promote the type ofx
along thetrue
branch foris
, and along thefalse
branch foris!
. Thecast
throws if the correspondingis
check would fail, and can promote a local variable's type on the non-throwing branch.Promotion only works for local variables, and only if the type being checked is a subtype of the current type of the variable. To check and promote something which is not a local variable, or which has a type which is not a supertype of the type being checked, you need to first create a new local variable and bind it to the value. That requires a statement, so it can't be done at the place where the expression would otherwise occur.
This feature will allow
is
andas
to introduce a new variable, and it allows assignments to existing variables to be promotable.Promotable Assignment
Currently the only expressions which causes
is
/as
to promote are identifiers denoting a local variable.We will extend that to also apply to expressions of the form
x = e
wherex
is a local variable, and expressions of the form(e)
wheree
allows promotion. This allows you to assign-and-promote in a single expression:Then you won't have to move the assignment out of a larger expression and potentially change the evaluation order.
This is not a very high impact feature, but it lays the ground for the even more usable feature below.
Binding Cast and Check
You can now bind and promote in-place, using
(x = e) is T
. The next step is to allow declaringx
in the check/cast.We use the syntax
e is T id
ande as T id
.The type check works just like
(id = e) is T
whereid
was a pre-existing variable declared in the same block, which was promotable toType
, but which could not be used at its unpromoted type. The scope of the variable is exactly the scope where it would be promoted to a type different from what it was declared as. It is like it's avoid
variable promoted toT
, it can only be used where it's not demoted back tovoid
. Thee is! T id
will bindid
along thefalse
branch, just asx is! T
will promotex
along thefalse
branch.The type cast also works as if it was
(id = e) as T
for a pre-declared variable which is unusable at its unpromoted type.In either case, the scope of the variable is the entire containing statement block (just as any other local variable declaration), and you are not allowed to use it before its declaring cast/check is evaluated. More precisely, you are only allowed to use the variable in code dominated by the declaration, which is exactly the same code where a promotion would apply.
Generalized Binding Syntax
The
T id
binding looks very much like a variable declaration, and we'll allow a few more syntactic constructors which look like variable declarations in the same place:e as var x
— Pure binding, no type check or promotion.e is final int x
— Final binding promotion. Some people preferfinal
.e as final x
; — Pure final binding.More precisely, what comes after
is
,is!
oras
is a<checkType>
defined as:This mimics the format of parameters or variable declarations, except that single identifier denotes a type, not a name. You need to add
var
in front to have a single variable.An expression of the form
e as var x
ore as final x
ore as SomeType x
is promotable, just as the assignment to an existing variable would be, soif ((e as var x) is int)
will promotex
toint
on thetrue
branch.Stronger Binding Cast
The
as
check usually requires parentheses,(e1.foo.bar.baz as Baz b).qux(b);
This reads badly, you have to look back a long way to see where the parentheses begins.
We introduce a stronger binding
as
cast:The parentheses ensure that the end of the type is unambiguous.
This allows you to write
e1.foo.bar.baz as(Baz b).qux(b)
.Possible Alternative Syntaxes
The example above doesn't read as well as one could hope.
The usual way to scan for the end of a selector sequence is to find the first space, skipping over parenthesized things because they are probably arguments.
Here the space occurs before the
as
, not inside parentheses.We can perhaps move the start parentheses before the
as
instead:yielding
e1.foo.bar.baz(as Baz b).qux(b)
.This allows the parentheses to fit snugly against the previous selector, which makes the expression easier to scan, but also makes it look, possibly too much, like a method call.
We could also use
<
and>
instead of parentheses, yielding one of:e1.foo.bar.baz as <Baz b>.qux(b)
, ore1.foo.bar.baz<as Baz b>.qux(b)
This looks less like a method invocation, but more like type arguments. However, the
<Baz b>
is untraditional in Dart for declaring a binding construct, compared to for examplecatch (e)
,for (var i;;)
andFunction(int i)
. Angled brackets is a place to put a type argument or type parameter, not a non-type-variable binding.Discussion
The promotable assignment is not really necessary, but it's consistent and makes it easier to explain what the binding check/cast does.
The scope of the variable is the entire block. This is consistent with how Dart currently treat local variables, but that can also sometimes be annoying. If we end up changing the scope of local variables to only be after their declaration, we would need special handling of expression-introduced variables. All other variable introductions happen at the statement level, and we don't consider reachability as an issue there. If a use of a variable is syntactically later than the declaration, and inside the same block, then the use is either dominated by the declaration, or it is unreachable, and we don't have to care. It's easy because all control flow constructs introduce a new syntactic scope. A conditional expression, or a short-circuit operator, does not.
Far-out extra feature: Generic Type Check
Example:
This could capture the run-time type argument of the type of
e
atList
, and expose bothT
andlist
, wherelist
has typeList<T>
.Not proposing to do that right now, but it means we should probably reserve the
e is<T>
syntax.The text was updated successfully, but these errors were encountered: