DanielXMoore / Civet

A TypeScript superset that favors more types and less typing
https://civet.dev
MIT License
1.33k stars 28 forks source link

`guard`-`else` #1215

Closed bbrk24 closed 1 month ago

bbrk24 commented 1 month ago

Now that we have isExit for other reasons, Swift-style guard seems implementable. At first glance, it seems identical to unless:

guard CONDITION else {
  STATEMENTS
}
// vvv
if (!CONDITION) {
  STATEMENTS
}

However, there are two key differences:

  1. If the condition creates any bindings (such as guard x? := getX() else { ... }), the bindings are only available outside the else block.
  2. STATEMENTS must be an "exit" of some sort, in order for 1 to be possible.

The primary use case is to prevent "pyramids of doom" when there are many bindings to be done:

if x? := getX()
  if y? := getY()
    if z? := getZ()
      setPoint x, y, z
// vvv
guard x? := getX() else return
guard y? := getY() else return
guard z? := getZ() else return
setPoint x, y, z
bbrk24 commented 1 month ago

Supersedes #756 I suppose

STRd6 commented 1 month ago

A couple thoughts:

I'm reluctant to add any more reserved words like guard. Would unless x? := getX() else return be an acceptable spelling of this feature?

Maybe upgrading pipes to better handle piping to return/throw and allowing something similar to declaration conditions could help with this. https://github.com/DanielXMoore/Civet/issues/897#issuecomment-2002531328

x := getX() !> return
---
let ref = getX(); if(!ref) return ref; const x = ref;

Possible conditional pipe options: ?>, !>, ?|>, !|> maybe something else?

bbrk24 commented 1 month ago

unless-else reads like a double-negative, but otherwise I'd be fine with it.

edemaine commented 1 month ago

Pipe conditions do seem like a reasonable possible approach. We might need one for truthy and one for nonnull. We've talked about ?|> before to act like "if nonnull, then pipe into rhs".

Given how much guard resembles unless, I'd be tempted to spell it like so:

unless x? := getX()
  throw new Error "Missing x coordinate"
unless y? := getY() then return
unless z? := getZ() then return
setPoint x, y, z

756 points out that our current meaning of unless x := ... isn't very helpful; it just exposes a falsey value for x inside the block. The proposed alternative would be to expose the declaration outside the unless block instead — essentially opposite of if, which kind of makes sense?

Alternatively, we could pull the declaration outside:

x := getX() ?? throw new Error "Missing x coordinate"
y := getY() ?? return
z := getZ() ?? return
setPoint x, y, z

This works for throw, but not for return, because we don't yet have a way to expressionize return. When it's at the top level of a declaration like this, I imagine we could handle it though.