Skip to content
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

Proposal: while with nullable unwrapping #357

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

Proposal: while with nullable unwrapping #357

AndreaOrru opened this issue May 3, 2017 · 16 comments
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. enhancement Solving this issue will likely involve adding new logic or components to the codebase. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@AndreaOrru
Copy link
Contributor

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
Copy link
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 Solving this issue will likely involve adding new logic or components to the codebase. label May 3, 2017
@andrewrk andrewrk added this to the 0.2.0 milestone May 3, 2017
@andrewrk andrewrk added the breaking Implementing this issue could cause existing code to no longer compile or have different behavior. label May 3, 2017
@raulgrell
Copy link
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

@andrewrk
Copy link
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
Copy link
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

@andrewrk
Copy link
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
Copy link
Sponsor Contributor

(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
Copy link
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
Copy link
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
Copy link
Contributor Author

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
Copy link
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
Copy link
Contributor Author

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

@andrewrk andrewrk modified the milestones: 0.1.0, 0.2.0 May 3, 2017
@raulgrell
Copy link
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);

@andrewrk
Copy link
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
Copy link
Sponsor Contributor

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
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
Copy link
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
Copy link
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
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. enhancement Solving this issue will likely involve adding new logic or components to the codebase. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

5 participants