Proposal: while with nullable unwrapping #357

Closed
AndreaOrru opened this Issue May 3, 2017 · 16 comments

Comments

Projects
None yet
5 participants
@AndreaOrru
Member

AndreaOrru commented May 3, 2017

Consider the following snippet:

var it = list.iterate();
while (it.next()) |node| {
    %%io.stdout.printf("{d}\n", node.data);
}

Iteration continues until the guard is not null. If it isn't, the unwrapped value gets assigned to the variable between |.

@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

A few notes:

  • The |node| is required, because if it wasn't needed then it could just read while (it.next() != null) {} as a normal boolean while loop.
  • If we follow this pattern, we can remove test for nullable types and use if in this same way. If the if statement has the |payload| then it's doing nullable unwrapping.
  • What about error types? Perhaps if we do the above then if and while should work with error union types as well. This has the downside of slightly less type safety.
Member

andrewrk commented May 3, 2017

A few notes:

  • The |node| is required, because if it wasn't needed then it could just read while (it.next() != null) {} as a normal boolean while loop.
  • If we follow this pattern, we can remove test for nullable types and use if in this same way. If the if statement has the |payload| then it's doing nullable unwrapping.
  • What about error types? Perhaps if we do the above then if and while should work with error union types as well. This has the downside of slightly less type safety.

@andrewrk andrewrk added the enhancement label May 3, 2017

@andrewrk andrewrk added this to the 0.2.0 milestone May 3, 2017

@andrewrk andrewrk added the breaking label May 3, 2017

@raulgrell

This comment has been minimized.

Show comment
Hide comment
@raulgrell

raulgrell May 3, 2017

Contributor

I'd be very happy to see if work the same way. test always felt like an awkward word for this purpose.

As for the error union types, you would only lose type safety in the else block, and only if you try to use the null value as the error value. A simple fix would be to only allow a | val | for the else block if the original value is an error union.

In other words:

if ( err_union ) | payload | { } else | err | {} // OK
if ( err_union ) | payload | { } else {} // OK

if ( maybe_T ) | payload | { } else | null_T | {} // Not OK
if ( maybe_T ) | payload | { } else {} // OK
Contributor

raulgrell commented May 3, 2017

I'd be very happy to see if work the same way. test always felt like an awkward word for this purpose.

As for the error union types, you would only lose type safety in the else block, and only if you try to use the null value as the error value. A simple fix would be to only allow a | val | for the else block if the original value is an error union.

In other words:

if ( err_union ) | payload | { } else | err | {} // OK
if ( err_union ) | payload | { } else {} // OK

if ( maybe_T ) | payload | { } else | null_T | {} // Not OK
if ( maybe_T ) | payload | { } else {} // OK
@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

One idea which would solve the type safety issue is for error unions we could require the else |err| (even if you do end up doing else |_|). Requiring this is similar to enforcing that you check for the error and at least acknowledge that there was one in the syntax.

Member

andrewrk commented May 3, 2017

One idea which would solve the type safety issue is for error unions we could require the else |err| (even if you do end up doing else |_|). Requiring this is similar to enforcing that you check for the error and at least acknowledge that there was one in the syntax.

@raulgrell

This comment has been minimized.

Show comment
Hide comment
@raulgrell

raulgrell May 3, 2017

Contributor

That would mean:

if ( err_union ) | payload | { } else | err | {} // OK
if ( err_union ) | payload | { } else | _ | {} // OK, discarding error
if ( err_union ) | payload | { } else {} // Not OK, must acknowledge error

if ( maybe_T ) | payload | { } else | null_T | {} // Not OK, no meaningful value
if ( maybe_T ) | payload | { } else | _ | {} // Not OK
if ( maybe_T ) | payload | { } else {} // OK
Contributor

raulgrell commented May 3, 2017

That would mean:

if ( err_union ) | payload | { } else | err | {} // OK
if ( err_union ) | payload | { } else | _ | {} // OK, discarding error
if ( err_union ) | payload | { } else {} // Not OK, must acknowledge error

if ( maybe_T ) | payload | { } else | null_T | {} // Not OK, no meaningful value
if ( maybe_T ) | payload | { } else | _ | {} // Not OK
if ( maybe_T ) | payload | { } else {} // OK
@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

Exactly, and notice that it's a perfect XOR, e.g. you can't accidentally do something with a nullable when you meant to use an error union, or vice versa.

Member

andrewrk commented May 3, 2017

Exactly, and notice that it's a perfect XOR, e.g. you can't accidentally do something with a nullable when you meant to use an error union, or vice versa.

@thejoshwolfe

This comment has been minimized.

Show comment
Hide comment
@thejoshwolfe

thejoshwolfe May 3, 2017

Member

(I started typing this when there were only 2 comments in this thread.)

The Java way to treat if is that only booleans are allowed. The Python way to treat if is that all types of things should be able to work as a boolean somehow. I'm extremely biased in favor of the Java approach. Even in C++, I don't like if (ptr) instead of if (ptr != nullptr).

When I look at code, I like to be able to know what the types of things are. If I see if (x) in Java, I know x is a boolean (or Boolean). If I see if x: in Python, I have no idea what's going on. x could be a bool, int, str, list, etc. and the programmer may have meant something meaningful in each of those cases, like if x != False:, if x != 0:, if len(x) != 0:, etc., or in any case if x != None:. This is somehow sold as a "feature" of Python, where it's easy to write "powerful" or "expressive" code or some shit, but it just ends up being confusing (and even error prone) in a language that supposedly emphasizes readability.

The above emotions are a big motivator for Zig's boolean-only rule in if conditions. I know that @andrewrk isn't quite as enthusiastic about this particular point as I am. The point of bringing all this up is that I want these concerns to be considered, and to be considered with a grain of salt.

This is all relevant to this issue because I don't want there to be a syntax that works for two (or a finite number of) different types without being clear which one it's operating on.

  • If I see while (a.foo()) { }, I want to be sure that foo() is returning a bool.
  • If I see if (x) { }, I want to be sure that x is a bool.

Since starting to type this rant, I see some cool proposals being discussed that perfectly fit my readability objective. Here's a list of if syntaxes from the perspective of type readability:

  • if (a) {} - a is bool.
  • if (a) {} else {} - a is bool.
  • if (a) |b| {} - a is ?@typeOf(b).
  • if (a) |b| {} else {} - a is ?@typeOf(b).
  • if (a) |b| {} else |c| {} - a is %@typeOf(b).
  • if (a) {} else |b| {} - a is %void.

And here's the while syntaxes following the proposal in the OP:

  • while (a) {} - a is bool.
  • while (a) |b| {} - a is ?@typeOf(b).

Here are some concerns:

  • if (a) {} else |b| {} might take a lot of scrolling to find the |b| to inform your interpretation of the type of a.
  • using if on an error type means I have to put the success handler before the error handler ("then" before else), which might not be what I want, like If I wanted to return on error. But maybe in this case I can use %% to handle the error, and return. Maybe this is ok.
Member

thejoshwolfe commented May 3, 2017

(I started typing this when there were only 2 comments in this thread.)

The Java way to treat if is that only booleans are allowed. The Python way to treat if is that all types of things should be able to work as a boolean somehow. I'm extremely biased in favor of the Java approach. Even in C++, I don't like if (ptr) instead of if (ptr != nullptr).

When I look at code, I like to be able to know what the types of things are. If I see if (x) in Java, I know x is a boolean (or Boolean). If I see if x: in Python, I have no idea what's going on. x could be a bool, int, str, list, etc. and the programmer may have meant something meaningful in each of those cases, like if x != False:, if x != 0:, if len(x) != 0:, etc., or in any case if x != None:. This is somehow sold as a "feature" of Python, where it's easy to write "powerful" or "expressive" code or some shit, but it just ends up being confusing (and even error prone) in a language that supposedly emphasizes readability.

The above emotions are a big motivator for Zig's boolean-only rule in if conditions. I know that @andrewrk isn't quite as enthusiastic about this particular point as I am. The point of bringing all this up is that I want these concerns to be considered, and to be considered with a grain of salt.

This is all relevant to this issue because I don't want there to be a syntax that works for two (or a finite number of) different types without being clear which one it's operating on.

  • If I see while (a.foo()) { }, I want to be sure that foo() is returning a bool.
  • If I see if (x) { }, I want to be sure that x is a bool.

Since starting to type this rant, I see some cool proposals being discussed that perfectly fit my readability objective. Here's a list of if syntaxes from the perspective of type readability:

  • if (a) {} - a is bool.
  • if (a) {} else {} - a is bool.
  • if (a) |b| {} - a is ?@typeOf(b).
  • if (a) |b| {} else {} - a is ?@typeOf(b).
  • if (a) |b| {} else |c| {} - a is %@typeOf(b).
  • if (a) {} else |b| {} - a is %void.

And here's the while syntaxes following the proposal in the OP:

  • while (a) {} - a is bool.
  • while (a) |b| {} - a is ?@typeOf(b).

Here are some concerns:

  • if (a) {} else |b| {} might take a lot of scrolling to find the |b| to inform your interpretation of the type of a.
  • using if on an error type means I have to put the success handler before the error handler ("then" before else), which might not be what I want, like If I wanted to return on error. But maybe in this case I can use %% to handle the error, and return. Maybe this is ok.
@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

using if on an error type means I have to put the success handler before the error handler ("then" before else), which might not be what I want, like If I wanted to return on error. But maybe in this case I can use %% to handle the error, and return. Maybe this is ok.

This is status quo.

One more syntax that was proposed:

  • while (a) |b| {} else |c| {} - a is %@typeOf(b).
  • while (a) {} else |b| {} - a is %void.
Member

andrewrk commented May 3, 2017

using if on an error type means I have to put the success handler before the error handler ("then" before else), which might not be what I want, like If I wanted to return on error. But maybe in this case I can use %% to handle the error, and return. Maybe this is ok.

This is status quo.

One more syntax that was proposed:

  • while (a) |b| {} else |c| {} - a is %@typeOf(b).
  • while (a) {} else |b| {} - a is %void.
@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

Consider this code:

var maybe_node = linked_list.first;
while (maybe_node) |node| {
    // ...
    maybe_node = node.next;
}

You should be able to put the maybe_node = node.next as the extra expression in the while loop, but it doesn't work because of scoping:

var maybe_node = linked_list.first;
while (maybe_node; maybe_node = node.next) |node| {
    // ...
}

Not sure how to solve this.

Member

andrewrk commented May 3, 2017

Consider this code:

var maybe_node = linked_list.first;
while (maybe_node) |node| {
    // ...
    maybe_node = node.next;
}

You should be able to put the maybe_node = node.next as the extra expression in the while loop, but it doesn't work because of scoping:

var maybe_node = linked_list.first;
while (maybe_node; maybe_node = node.next) |node| {
    // ...
}

Not sure how to solve this.

@AndreaOrru

This comment has been minimized.

Show comment
Hide comment
@AndreaOrru

AndreaOrru May 3, 2017

Member

In theory, you could evaluate the extra expression on the while loop in the scope of the block - where node is unwrapped.

Member

AndreaOrru commented May 3, 2017

In theory, you could evaluate the extra expression on the while loop in the scope of the block - where node is unwrapped.

@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

In theory, you could evaluate the extra expression on the while loop in the scope of the block - where the node is unwrapped.

Right, but that would look incorrect. It would be the only place syntactically that you don't read left to right, top to bottom, to determine what's in scope.

Here's another proposal:

Instead of

while (condition; expression) {}

have

while (condition) : (expression) {}

and the : (expression) is optional. We need the parens for the same reason we need the parens for if, while, for, etc., which is that symbol { } is struct initialization syntax.

Then the example becomes:

var maybe_node = linked_list.first;
while (maybe_node) |node| : (maybe_node = node.next) {
    // ...
}

A loop to count to 10 looks like:

var n: usize = 1;
while (n <= 10) : (n += 1) {
    %%io.stderr.printf("{}\n", n);
}

Maybe ; instead of :?

Member

andrewrk commented May 3, 2017

In theory, you could evaluate the extra expression on the while loop in the scope of the block - where the node is unwrapped.

Right, but that would look incorrect. It would be the only place syntactically that you don't read left to right, top to bottom, to determine what's in scope.

Here's another proposal:

Instead of

while (condition; expression) {}

have

while (condition) : (expression) {}

and the : (expression) is optional. We need the parens for the same reason we need the parens for if, while, for, etc., which is that symbol { } is struct initialization syntax.

Then the example becomes:

var maybe_node = linked_list.first;
while (maybe_node) |node| : (maybe_node = node.next) {
    // ...
}

A loop to count to 10 looks like:

var n: usize = 1;
while (n <= 10) : (n += 1) {
    %%io.stderr.printf("{}\n", n);
}

Maybe ; instead of :?

@AndreaOrru

This comment has been minimized.

Show comment
Hide comment
@AndreaOrru

AndreaOrru May 3, 2017

Member

I actually really like this. And : is better IMHO.

Member

AndreaOrru commented May 3, 2017

I actually really like this. And : is better IMHO.

@andrewrk andrewrk modified the milestones: 0.1.0, 0.2.0 May 3, 2017

@raulgrell

This comment has been minimized.

Show comment
Hide comment
@raulgrell

raulgrell May 3, 2017

Contributor

I quite like this as well, ; is usually associated with the termination of a statement so I think it would look out of place here. However, I don't like the idea of using : here because the colon usually goes next to a type or a label (see also #346). Could we use the do keyword?

while (n <= 10) do (n += 1) {}
while (maybe_node) |node| do (maybe_node = node.next) {}

For completeness, a regular do-while would use this, but you wouldn't be able to unwrap with a do-while.

var n: usize = 1;
do {
    %%io.stderr.printf("{}\n", n);
} while (n <= 10) : (n += 1);

// Above is fine, null unwrap is weird. Fine because it makes no sense not to unwrap.
var maybe_node = linked_list.first;
do {
     // node not defined
} while (maybe_node) |node| : (maybe_node = node.next);

Contributor

raulgrell commented May 3, 2017

I quite like this as well, ; is usually associated with the termination of a statement so I think it would look out of place here. However, I don't like the idea of using : here because the colon usually goes next to a type or a label (see also #346). Could we use the do keyword?

while (n <= 10) do (n += 1) {}
while (maybe_node) |node| do (maybe_node = node.next) {}

For completeness, a regular do-while would use this, but you wouldn't be able to unwrap with a do-while.

var n: usize = 1;
do {
    %%io.stderr.printf("{}\n", n);
} while (n <= 10) : (n += 1);

// Above is fine, null unwrap is weird. Fine because it makes no sense not to unwrap.
var maybe_node = linked_list.first;
do {
     // node not defined
} while (maybe_node) |node| : (maybe_node = node.next);

@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 3, 2017

Member

do looks good as a replacement for :, but I don't think it makes sense to have that role there, and then a different role in a do...while statement.

I'm not a fan of do...while, because it's barely different than:

while (true) {

    if (condition) break;
}

I feel like do...while is trying to bend the syntax over backwards to accommodate what can be accomplished with a more explicit control flow.

Member

andrewrk commented May 3, 2017

do looks good as a replacement for :, but I don't think it makes sense to have that role there, and then a different role in a do...while statement.

I'm not a fan of do...while, because it's barely different than:

while (true) {

    if (condition) break;
}

I feel like do...while is trying to bend the syntax over backwards to accommodate what can be accomplished with a more explicit control flow.

@thejoshwolfe

This comment has been minimized.

Show comment
Hide comment
@thejoshwolfe

thejoshwolfe May 4, 2017

Member

Just to throw in the ol' josh-style super verbose but pretty readable and correct syntax proposal:

while (a) continue: (b) {}
while (a) |b| continue: (c) {}

here it looks like you're declaring a label called continue, which is pretty reasonable. that's a lot of typing though.

I'm not a fan of the while-do proposal, because it looks like the do clause is what gets executed every iteration. similar to Bash's for a in b; do c; done, or the way you'd use do and done in Ruby (i think).

Member

thejoshwolfe commented May 4, 2017

Just to throw in the ol' josh-style super verbose but pretty readable and correct syntax proposal:

while (a) continue: (b) {}
while (a) |b| continue: (c) {}

here it looks like you're declaring a label called continue, which is pretty reasonable. that's a lot of typing though.

I'm not a fan of the while-do proposal, because it looks like the do clause is what gets executed every iteration. similar to Bash's for a in b; do c; done, or the way you'd use do and done in Ruby (i think).

andrewrk added a commit that referenced this issue May 4, 2017

change while syntax
Old:

```
while (condition; expression) {}
```

New:

```
while (condition) : (expression) {}
```

This is in preparation to allow nullable and
error union types as the condition. See #357
@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 4, 2017

Member

I agree this continue: syntax is readable and correct. I feel like we're already kind of walking on thin ice here with verbosity and while (a) : (b) {} is a reasonable compromise.

Member

andrewrk commented May 4, 2017

I agree this continue: syntax is readable and correct. I feel like we're already kind of walking on thin ice here with verbosity and while (a) : (b) {} is a reasonable compromise.

@andrewrk

This comment has been minimized.

Show comment
Hide comment
@andrewrk

andrewrk May 4, 2017

Member

Now that while expressions have else nodes, it might make sense to have break take an expression parameter like return. So you can have something like:

const result = while (bar()) |value| {
    if (value > 50)
        break value;
} else {
    10
};
Member

andrewrk commented May 4, 2017

Now that while expressions have else nodes, it might make sense to have break take an expression parameter like return. So you can have something like:

const result = while (bar()) |value| {
    if (value > 50)
        break value;
} else {
    10
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment