Skip to content

Proposal: while with nullable unwrapping #357

Closed

Description

@AndreaOrru

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 |.

Activity

  1. andrewrk commented on May 3, 2017

    @andrewrk
    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.
  2. added
    enhancementSolving this issue will likely involve adding new logic or components to the codebase.
    on May 3, 2017
  3. added this to the 0.2.0 milestone on May 3, 2017
  4. added
    breakingImplementing this issue could cause existing code to no longer compile or have different behavior.
    on May 3, 2017
  5. raulgrell commented on May 3, 2017

    @raulgrell
    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
    
  6. andrewrk commented on May 3, 2017

    @andrewrk
    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.

  7. raulgrell commented on May 3, 2017

    @raulgrell
    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
    
  8. andrewrk commented on May 3, 2017

    @andrewrk
    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.

  9. thejoshwolfe commented on May 3, 2017

    @thejoshwolfe
    SponsorContributor

    (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.
  10. andrewrk commented on May 3, 2017

    @andrewrk
    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.
  11. andrewrk commented on May 3, 2017

    @andrewrk
    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.

  12. AndreaOrru commented on May 3, 2017

    @AndreaOrru
    ContributorAuthor

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

  13. andrewrk commented on May 3, 2017

    @andrewrk
    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 :?

  14. AndreaOrru commented on May 3, 2017

    @AndreaOrru
    ContributorAuthor

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

  15. modified the milestones: 0.1.0, 0.2.0 on May 3, 2017
  16. raulgrell commented on May 3, 2017

    @raulgrell
    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);
    
    
  17. andrewrk commented on May 3, 2017

    @andrewrk
    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.

  18. thejoshwolfe commented on May 4, 2017

    @thejoshwolfe
    SponsorContributor

    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).

  19. added a commit that references this issue on May 4, 2017
  20. andrewrk commented on May 4, 2017

    @andrewrk
    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.

  21. andrewrk commented on May 4, 2017

    @andrewrk
    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
    };
  22. added
    proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.
    on Sep 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    breakingImplementing this issue could cause existing code to no longer compile or have different behavior.enhancementSolving this issue will likely involve adding new logic or components to the codebase.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions