-
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
An alternative syntax for if-variables #1975
Comments
I think this is a step in the wrong direction. |
I agree my proposed syntax is pretty magical. This is definitely an interesting approach for us to play with. Right now, I find it more confusing, but that could be familiarity. Here's what tripped me up:
Here, there's nothing to clearly indicate that At least with: if (var points[0].x is int) print(x.isEven); A confused reader can probably correctly guess that With: if (var x from points[0] is int) x.isEven; I think it might be too easy for users to mistakenly guess that it means something like: if (points[0] is int; var x = points[0]) x.isEven; |
I don't understand what you're trying to say here, can you elaborate? I don't understand why it's hard to "guess wrong" with the old syntax. An easy way to "guess wrong" is just to not notice the
Maybe. Perhaps a different syntax than
I don't believe either of those statements are true about the original proposal, so I question the statement that it's hard for the user to guess wrong. In any case, I think we may want some user data on this. I continue to find code like |
IMHO any prefix kind of My first thoughts when reading these lines alone:
Please, consider a suffix style. if (foo.bar is int daz) {
print(daz + 1);
} I think we can have a better version of it: if (foo.bar is int var daz) {
print(daz + 1);
} This line: if (foo.bar is int var) {
print(bar + 1);
} This is nice but then it smells magic.
if (foo.bar is int use) {
print(bar + 1);
}
if (foo.bar is int use daz) {
print(daz + 1);
} |
Serious question: why is that better than this? var daz = foo.bar;
if (daz is int) {
print(daz + 1);
} This version has two extra characters, and is something you can do today, so why add a whole new syntax for it? |
Agreed. What's less clear (I believe) is that the value of that variable is. And that's a larger concern to me because it's a runtime property of the program. They may think it has some value, think they are reading the code correctly, and have it do something unexpected at runtime. With the if-var syntax, if the reader is confused, they are confused about the static semantics and will notice their confusion before they try to run it. Again, though, I agree if-var syntax is fairly magical too. It's hard to do anything that's more terse than just explicitly declaring a local variable without introducing some significant level of magic. |
Scoping is pretty relevant to the runtime behavior of the program.
I just don't see this. I agree that they are confused about scoping, but how is that better? Let's try an example. Here's some existing code: class Foo {
int? x;
void test(Foo other) {
if (var other.x != null) {
// Stuff
}
} Hmm, I think when class Foo {
int? x;
void test(Foo other) {
if (var other.x != null) {
// Stuff
x = other.x;
}
} Why doesn't this execute in the way that I expect? Because the scoping is very relevant to the execution of the program: the fact that this said I hear the critique of this alternative syntax - I agree that there is a potential confusion to make when reading it. I don't really understand the argument that it is a different category or severity of confusion - to the contrary, I believe that confusion about scoping is one of the most fundamental confusions one can have about a program. Correct resolution of variables is probably the most fundamental cognitive task in programming - without it, you cannot even build a correct syntactic model of the program in your head, much less a correct semantic model. |
FWIW, if we're not going to try to solve the "having to choose and bind a new name" problem (which is what the if-variables proposal was intended to solve) but we do believe that "locality" of the binding is worth having syntax for, something in this direction is what I'd be in favor of. But I'm not yet convinced that moving the |
I'm speculating about the mental state of users, but with the original proposed syntax, if they are confused, their confusion is more like to force them to stop and learn the feature before they proceed. I worry that with the syntax you propose here, they may be confused, think they understand the syntax, and then proceed to believe their program means something it doesn't. I could be wrong, but that's my gut feel from looking at the syntax. |
I added another exploratory proposal to consider using pattern matching to avoid what I see as the orthogonal faults with these two proposals here. |
I'll admit that I find If it was What If we want to highlight the name in the declaration, which is what I think the If the only expression that can follow It's not awesome syntax, The syntax might allow About scoping, I'd very much like the variable to be in scope an place inside the same block/construct that is also dominated by the introduction. (Basically, where an uninitialized local variable in the same block/structure would be definitely assigned by an assignment in the same place as the if (var o.someFutureOr is Future<T>) {
// someFutureOr is Future<T>
} else {
// someFutureOr is T
} |
IMO there are a couple of reasons... One is boredom. But I do agree with you that the examples you quoted aren't the most productive, that is the reason that I would rather use the Map foo(Bar bar, [Foo? foo]) => {
if (bar.xyz is Foo use)
'name': xyz.name,
if (foo?.xyz != null use)
'secondName': xyz.name
} |
I actually don't think it's very hard to read the @lrhn wrote:
void main() {
var s = 'This is a long string of text';
print('${s.var:substring(8)} ${substring.var s1:substring(7)} $s1 $s1 $s1.');
// Prints 'a long string of text string of text string of text string of text.'
} It is of course a rather unprincipled approach to use the method invocation as the source of the value of the new variable (rather than tearing off the method), but I suspect that it's much more usable in practice. |
I really wish I didn't have to type a full declaration inside an |
@tatumizer wrote:
The goal is to allow us as developers to capture the value of an expression used as a part of a bigger construct (say, a bigger expression or an class C {
num n;
...
void foo() {
if (var:n is int) {
n.isEven; // The new variable is in scope and promoted.
} else {
foo(n); // The new variable is in scope.
}
// The new variable is out of scope here.
}
So why not? ;-) |
what about: if (obj.@prop != null)
print(prop); // prop local and not null
if (obj.@prop is Foo)
print(prop); // prop is Foo for array index checks: if (obj.elements@{i}[0] != null) {
print(i); // i is local and not null
} property checks like |
"Readable" is a very subjective word :) I actually like Doesn't read that well inside an actual (Personally, I like the in-place |
We could also compare it with the following: if (var r: rechtsschutzversicherungsgesellschaften is int) {
r.isEven; // The new variable is in scope and promoted.
} else {
foo(r); // The new variable is in scope.
} Nobody says you have to use the existing name, that's probably not required for any of these many proposals. ;-) |
Who knows? Shadowing should be frowned upon when it occurs by accident, such that one declared entity is far too easily believed to be another one, typically with no other connection between the two entities than the unfortunate choice of the same identifier. That's a footgun. However, in the case where the two entities are closely related it could be a useful style rule to encourage shadowing. For example, we may need to access a given object using a local variable such that we can use promotion, and we'll initialize that variable by evaluating a non-local variable. In that case the two variables are conceptually "the same thing". This is definitely a very different situation than the footgun mentioned earlier. If the two variables do end up having different names (for whatever reason) then we might actually want to pair up the two names by making them similar (e.g., In other cases we're not initializing the new variable from an existing one (e.g., we could initialize it from any expression, and we could take a default name for the variable from part of that expression; for instance, getter invocations like
Well, you can't really deny that Familiarity is really difficult to prioritize. It may help you for about 5 seconds, and it's important for the community that as few as possible are turning away from the language immediately because it is full of unfamiliar constructs. But I do think that long term qualities are more important than familiarity. Being intuitive basically means having properties (affordances) that are quickly and effortlessly available to the mind of a reader of the source code: You don't have to think hard and in terms of explicit reasoning in order to understand how to use it. I think that may occur because of familiarity, but, more importantly, I think it may occur in a more profound sense because of semantic consistency: It may well be based on an implicit and approximate kind of thinking (that's what makes it "intuitive" rather than just "logical and consistent"). But if the language can be understood in terms of a minimal number of semantic rules that are applied consistently across many different language constructs and concepts, then a reader of the source code can rely on those rules to understand systematically and precisely what a given snippet of code does. That's basically the notion of orthogonality in language design. On top of this comes the need for abstraction: If we eliminate nearly all language features (think: BASIC 1977) then the code is immediately easy to understand, line for line, but it may be a lot harder to understand a larger software artifact like a whole program, because we're drowning in a large amount of repetitive code, unable to spot the big picture. So familiarity is always a good starting point, but if we need to be a little bit unfamiliar in order to have a consistent language design then I honestly do believe that we should keep the long term qualities of the language in mind. When it comes to The trade-off is not simple. That's probably the reason why we're having so much fun. ;-) |
The two-clause If anything, it's more important for if (var x = this.x; x != null) ... and var x = this.x;
if (x != null) ... is tiny, you can always have a declaration just before the Node node = ...;
while (var next = node.next; next != null) {
node = next;
} You can't do that by moving the variable outside (or rather, you can because it's nullable, then it's: Node node = ...;
Node? next;
while ((next = node.next) != null && next != null) {
node = next;
} (The For the conditional expression, you can't have a declaration just before the expression. So, We also have to define what the scope of the variable is, because we do want it to extend to the body of the If we only allow the variable inside tests and extend the scope to the following branches, then it's well-defined. HOWEVER (ran out of emphasis there), if we allow it for two-clause tests in the conditional expression, we have effectively introduced an ugly (var x = anything; false)?_: somethingUsing(x, x) where That's an expression local declaration. It's ugly, but it's there. If we have the functionality anyway, we should embrace it and let you have a non-ugly version too. So, as @mit-mit suggested to me, what if expression ::= declaration_expression | expression_no_decl
declaration_expression ::= var_declaration `;' expression_no_decl
expression_no_decl ::= ... current expression ... and we define that the scope of the declaration is the following expression and any construct can say that variables definitely declared in one part also applies in another:
(Maybe "is a declaration expression, possibly wrapped in any number of parentheses"). |
@jodinathan Thanks for walking through your reasoning. I agree there's a possibly non-rational but nonetheless very real difference there. Another way of phrasing it in my mind is that since you end up tying the variable declaration to the |
This is, of course, just a syntax for a
This starts to feel kind of ad hoc, but maybe I could learn to live with it. I think you probably want the variable to apply in the failure continuation of an |
I would like to point out that if we are not going for a suffix style, then the @eernstg proposal seems more robust in my opinion. It is a different syntax, however, it leads to less doubts on what is happening:
I also think we could think on how to make it shorter. Maybe using something like if (obj.@prop != null)
print(prop); // prop local and not null
if (obj.@prop is Foo)
print(prop); // prop is Foo
// naming fits nicely because it is very close to string concatenation pattern:
if (obj.elements@{i}[0] != null) {
print(i); // i is local and not null
} |
One of the primary concerns with the original if-vars issue was that the code can be surprising to those that try and read it without knowing about the feature. I think that is a fair concern. I think this issue is caused by using a familiar keyword ( It would look like this in the case with renaming: if (let m = myNullableField; m != null) {
m.method1();
} It would be nice to also support not having to come up with a new variable name if you don't want one. Based on earlier discussions of other places where we'd like to refer to something that hasn't been named (e.g. #265), how about using if (let myNullableField != null) {
it.method1();
} |
what about the Zig If 's? if (obj.prop is Foo) |prop| print(prop);
if (first != null && second != null) |one, two| {
print(one + two);
}
if (first != null && second != null) |_, it| {
it.call();
}
if (first != null || second != null) |it| {
fn(it);
} or if (obj.prop is Foo) : obj.prop {
print(prop);
}
if (first != null && second != null) : one, two {
print(one + two);
}
if (first != null && second != null) : _, it {
it.call();
}
if (first != null || second != null) : it {
fn(it);
} |
@mit-mit wrote, about in-expression declarations using
Indeed, but I don't think it's realistic to expect any language mechanism to be precisely "guessable" at first sight, it's reasonable to require the language mechanism to be introduced to every developer who's going to use it, at least briefly. But it would be helpful if we can rely on existing knowledge to provide most of the information. Note that it was part of the design of the binding expressions (#1210) that they use |
I've split out the proposal for if-scoped variables into an issue here. I think all of the other basic proposals are covered by some issue somewhere. I'll leave this issue open for a bit longer in case there's any follow on discussion folks want to have, but I think it's clear there's not much buy in on the team for the approach described here. |
In this issue, @munificent proposes a way of re-using a property name as a local variable in a specific scope to enable promotion on properties. The core idea is appealing, but I find the syntax surprising and unapproachable. I find it hard to believe that a developer not already familiar with the feature would understand what was happening with (for example) this code:
This issue is to explore an alternative syntax for the same feature. That is, I don't propose any changes to the underlying semantic notion - merely a change in syntax. The above proposal re-uses the existing property access syntax to produce a new variable binding syntax. I propose instead to start with the existing variable binding syntax and add a new way access a property. Specifically, I propose to replace the general syntactic form
var e.x is T
withvar x from e is T
;var e.x == null
withvar x from e == null
; and similarly for the negative forms. Whene
is an implicitthis
access, I propose to require that thethis
be explicitly specified (more on that below).Simple Example
Continuing by example, using the examples from the above proposal.
Here, I require the implicit
this
to be made explicit. We could choose not to do this for the sake of brevity, but I believe that making this explicit makes the code much more readable. I find theif( var obj is int)
syntax particularly impenetrable, since it is so very close to the existing variable binding syntax. I also believe that the vast majority of cases of interest are not accesses onthis
, but rather accesses on other objects.Promoting on null checks
Promoting on getters
Negative if-vars
Worked example
New syntax for the worked example from here:
New syntax from the worked example from here.
cc @munificent @lrhn @eernstg @jakemac53 @natebosch @stereotype441 @mit-mit
The text was updated successfully, but these errors were encountered: