3 ms·
Breakelse seems completely nihilistic to me. Here's an example: if (1 == 1) { if (random_global == 4) { breakelse } } els
by camgunz 3y ago
Breakelse seems completely nihilistic to me. Here's an example:
if (1 == 1) {
if (random_global == 4) {
breakelse
}
}
else {
...
}
I can imagine cases where inside of an if-block you want to jump into the other block, but those are cases where you need to restructure your control flow and state so that your conditionals are coherent. Providing breakelse as a trapdoor to escape is semantically catastrophic to the concept of if/else. You can never again scan for if conditionals and rule them out; you always have to understand the entire block (and probably as a result the state of the entire program) before you can rule it out.
---
Overall though, I think these are issues of semantics rather than issues of syntax. It's kind of an ongoing concern to figure out how to use railway oriented programming in an imperative language. You have two problems. First, you have to pick a side of the semipredicate problem:
- do you add complexity to your backend to do tuple returns that are mostly fast, knowing that people will largely use them after they're not fast (i.e. too large to fit in a register)
- do you just use -1/NULL and learn to love the semantic graveyard you now live in
- do you use output parameters and make your language look 50 years old
- do you add complex syntax sugar to make tuple returns act like output parameters
Second, you either have to settle on an existing subset of goto, figure out a new subset of goto, or... not be imperative. Existing subsets include "if", "match"/"switch", and exceptions. Non-imperative solutions include do-notation, "everything is an expression", or callbacks. These aren't completely orthogonal; for example Python will do what you want with the new walrus operator:
def op(val):
if callable(val): # Sticking with the somewhat odd example here
return 'first'
if val == 'first':
return False
if val == 'second':
return 'third'
if (var := op) and (var2 := op(var)) and (var3 := op(var2)):
print(var, var2, var3)
else:
print('something failed, but what?')
The problem though, as you can see at the end, is it's not super clear where in your pipeline things exploded. You can of course imagine solutions to this, but (as always) we're verging towards full monads.
---
To my mind, I think you probably can't do better than the Result pattern in Rust (as long as you're willing to have a parameterizable type system). The "?" operator lets you cram everything on a single line, but even without it (I don't really like "?" as an operator) your "error handling" generally avoids the pyramid of doom. Go is similar; though people hate the "if val, err" pattern it's essentially the same; they're just objecting to syntax or pining for exceptions to bail them out of error handling at all.
There is something very ergonomically satisfying about getting something and testing it in a single conditional. Once you get used to it it's hard to give it up. But I don't really think it's a good pattern for larger state handling or error handling. Other languages split the difference with if-let or match (or both), which feels a little more "right tool for the job" to me.
- FeepingCreature 3y agoYep, this is deliberate to some extent. Your first comment is entirely correct; as I called out in the article, `breakelse` in its plain form is almost completely useless. Its raison d'etre in the language is largely as background glue for `?`, which is the actually important feature. And in the compiler itself, almost every use of `?` is `? else die` - ie. as runtime assert backing for assumptions that are true, but cannot be proven in the limited C-like typesystem, or as `? else return fail("error message")`. I almost never actually chain two `?`s together. Though to some extent, this is because I do my best to avoid nullable expressions in favor of structural typing to begin with. Basically, if the point in the pipeline where things exploded is relevant, this is the wrong feature. It's aimed at a situation where almost always, every step in the `?` property chain will go through, but we aren't able to statically prove this fact to the compiler, so in some cases we want to just branch out to a fallback path where we presumably log some debug info and then die more or less gracefully. Note that Neat has a completely different mechanism for error handling that actually propagates more detailed data, which can of course be freely intermixed with `:else`: nothing stops you from reacting to a failure explicitly with `.case(:else: return Error("entry not found"))` or `.case(null: return NullPointerError())`. Or inversely, `case(Error: breakelse)`. `breakelse` is more about letting a function decide that, by and large, if the problem happens you're not going to care too much about reporting why. But that's why I have `breakelse` as a separate keyword: because `?` is put together out of the two lego pieces of `case` and `breakelse`, you can, if you want, put them together in a different configuration with little extra effort.
- camgunz 3y ago> It's aimed at a situation where almost always, every step in the `?` property chain will go through, but we aren't able to statically prove this fact to the compiler, so in some cases we want to just branch out to a fallback path where we presumably log some debug info and then die more or less gracefully. Yeah, my head canon for this idiom is "or die", which is like a bashism/Perlism (maybe a little odd because I've spent maybe 30 hours w/ Perl 5). I kind of like Go's "must" idiom, where functions prefixed with "must" panic by convention when things go wrong, but as you point out it's not compile time. It looks like I didn't read too carefully before; I assumed this was in the context of some kind of boolean conditional because it was inside "if", but "if" here is doing pattern matching because it introspects types to see if they have an ":else", which means it's doing essentially what like, Rust's Result does. Am I wrong here? I think your example essentially boils down to: match op(op(op?)?) { Some(var3) => { /* do if stuff here */ } _ => { /* do else stuff here */ } } I think this is essentially, yeah, Result and pattern matching. > But that's why I have `breakelse` as a separate keyword: because `?` is put together out of the two lego pieces of `case` and `breakelse`, you can, if you want, put them together in a different configuration with little extra effort. I can't think of an instance where I'd be happy to see code jump out of an "if" with "breakelse". It seems like if you're going full result types then you need to go full pattern matching. I think that's how you give your users the ability to reconfigure this stuff. The syntax for it can be a little clunky, but most other languages get around it w/ stuff like "?" and "if let".