Breakelse: When Compiler Developers Get Bored
neat-lang.github.io
neat-lang.github.io
if( a )
{
if( b )
{
if( c )
breakelse;
}
else
{
// E2
}
}
else
{
// E1
}
You wrote it, tested it and are happy.Then a year later other person is reworking the code due to changed requirements, deletes the E2 else block, and breakelse suddenly starts to jump into E1 else block.
Especially bad if there's a page of code between breakelse and E2 block, and another page between E2 and E1. The proposed feature becomes a spooky action at a distance.
'outer: if a() {
'inner: if b() {
if c() {
breakelse 'inner;
}
if d() {
breakelse 'outer';
}
} else {
// E2
}
} else {
// E1
}
This way, you can trigger syntax errors when someone refactors the code. It's not particularly pretty, but it does communicate the control flow better than simple gotos in my opinion.Thinking about this, I imagine there's even a way this could be made useful: in languages where if/case/match statements are expressions, you could make breakelse an expression that returns the result of the else branch. On the other hand, perhaps a "checkagain" statement to re-evaluate the if statement once would be better for that:
if isInDatabase(id) && isProfileComplete(id) {
return db.fetchUser(id);
} else {
api.autocompleteUser(id);
let retry = checkagain;
if retry != None {
db.update(retry);
}
}
Honestly I think it's ugly as hell but I think there could be something here if someone competent at designing languages would take a look at this. while (Console.ReadLine() is string line &&
int.TryParse(line.Trim(), out int number))
{
Console.WriteLine($"{line} is {number}");
}
This loop can be interrupted by ReadLine() returning null (failing the pattern-matchi as string line) or by int.TryParse returning false.One option is to make this a first-class feature of the language, rather than sugar for something else.
Icon (from 1977) is interesting here.
https://en.wikipedia.org/wiki/Icon_%28programming_language%2...
Throw an exception means "throw away the context of the failure and then goto some other function". The exchange amounts to:
Something went wrong somewhere!
Where? What?
I knew a moment ago but threw it away for implementation convenience. Here's an object.
Thanks!
Fringe benefits include sacrificing linear types - the zero runtime cost, infallible way of doing deterministic resource cleanup - and embracing the mental dissonance of goto evil unless it doesn't say where it's going to.
Sum types are probably the right thing.
while {
line: data.eat? "(.*)\n"
line: line.strip ()
print "We have read " + line
}
print "No more lines to eat."
The nice thing about it is that you're not limited to one conditional at the start of your "if"/"while" block. Have you ever a complex condition that's unwieldy to write in a single expression? if (request.is_logged_in() && db.has_user(request.get_user_id()) && db.get_user(request.get_user_id()).authorize() && ...) {
// How do I refer to "user" now?
}
becomes try {
user_id: request.get_user_id? ()
user: db.get_user? user_id
user.authorize? ()
print "Found user " + user.name
}, {
error UNAUTHORIZED
}
Which acts like a guard, without having to declare a new function. It can also replace the "walrus operator", nested try-catches, etc.The thing about `?` being built on basic primitives is that you can always just decide to define your own sumtype error handling.
Anyways, `:else` is more about "expected failures" than "errors". Loop-enders rather than process-enders.
catch
(do
user_id <- get_user_id <$> request
user <- user_id . get_user <$> db.get_user
authorize user
print $ "Found user " <> user ^. name)
(\e -> print "Error unauthorized)
I don't want to say it.. but both this and Breakelse are rediscovering a basic use case of Monads :)I am aware of Monads (they're explicitly called out in the article!), and specifically want to go in a different direction.
The feature itself looks the same as Erlang’s maybe expression: https://www.erlang.org/blog/my-otp-25-highlights/#selectable....
I wonder what other languages have this.
This seems more like
if (cond)
<do stuff>
if (anothr_cond)
my_else()
else
<do more stuff>
end
else
my_else()
end
but with syntax sugar so that the nested ifs are tucked away (and perhaps with my_else() scoped to the parent if).The big idea is that usually the `else` will do a nonlocal exit, so we should try to rewrite this in the "flat/early-out style." But then if you're declaring intermediate variables, you end up with duplicate lines:
auto var = op;
if (!var.cond) return my_else;
auto var2 = op2(var);
if (!var2.another_cond) return my_else;
do more stuff;
There's this sort of very regular block in there, of "declare variable, test condition, nonlocal exit".So the idea was to combine the `if (!var.cond) return my_else` into one expression-level location, so it turns out as
auto var = op? else return my_else;
auto var2 = op2(var)? else return my_else;
<do more stuff>
// Or instead
if (auto var = op?) {
auto var2 = var.op2?;
<do more stuff>
}
Then the question was, what's the best way to get both of those? And it occurred to me what I actually wanted was just a way to branch out of the if entirely, I didn't really care about preserving any values in the alternate case, I just wanted to tear the entire expression/scope down and do something else. Optional.of(op)
.filter(v -> !v.cond)
.map(v -> op2(v))
.filter(v2 -> !v2.cond)
.map(/* do more stuff */)
.orElseGet(() -> my_else)Because they're convenient and useful constructs. I don't know if `breakelse` is too, but maybe -- especially when part of a `?` operation.
Yeah I sometimes see this sentiment that you don't really need something roughly like sum types and higher-order functions to do things like chaining on them; you just need this one little special-cased keyword that will do 95% of what you want.
Do you have precedent for something like breakelse as an explicit operation?
The novelty is not making goto worse, it makes it better.
Let me rephrase your statement and see whether the claim that this isn't worth calling it novel stoll stands:
> It is good old "goto" once found in many early programming languages, which overtime prove too dangerous even in the hands of great coders hence faded away. The author proposes a safe, goto in disguise, mechanism that can be used as optimisation.
May I ask how many novelty aren't either a simple iteration over some existing idea, bringing together some existing ideas, or removing some undesired side effect of an idea
Rather than something like this:
if let Some(a) = option_a; let Some(b) = a.option_b {
...
} else {
...
}
you'd have something like this: if let b = option_a?.option_b? {
...
} else {
...
}
(Pseudocode for both examples.) 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.
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.
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".
If you have `Optional<Foo>`, and you access `foo?.field`, then the type is not `Optional<Field>`, but just `Field` - because in the None case, you've already exited the expression. So it's not `(foo)?.(field)`, but `(foo?).field`. To steal a phrasing from another comment, the non-local flow of `?` effectively everts the Optional foo.
I like result types, but I do think they're clunky because you have to handle them in a generic way (pattern matching) or add some (incongruous, IMO [0]) syntax sugar to handle them. Modeling them w/ a keyword and some syntax avoids that weird layering violation so I can support this. I guess my only beef here is giving any access to "breakelse" at all. Like, if you need the concept in the compiler OK, but I'm worried that outside the need to support "?", "breakelse" is a mental model annihilation machine. A better name would be "justkidding" or "sike" haha.
> Sort of - I'm specifically avoiding/getting rid of result types.
Whaaaaaa? I think you still have them because you wrote:
> That is, if would see that there was a possibility of an :else return type, and use this opportunity to jump to the else block instead of declaring a variable.
Or in the "eatLine" example, "eatLine" has to return Result<string> because you don't know if it'll always be called with "?". That's the ":else return type". You either need some kind of--very narrow--checked exceptions or whatever to enforce "?", or code has to know about the :else return type.
OK with that in mind, I now think you're talking about exceptions because of the type conundrum. There's another example someone posted about their language and a "while" loop (which is a pretty cool idea). After working with it a bit in this context, I didn't find it really any different than a try, e.g.:
try {
string line := eatLine()
string[] words := split(line)
int[] swear_words := get_swear_word_indices(words))
...
catch { ... }
Maybe there's an argument here that like, all Neat is doing is some implicit Result typing and exceptions are a world away, but I think they're closer than they seem because the behavior you're aiming at is the essence of exceptions: short-circuit on error. I will admit there's a significant difference between what you've implemented and the rest of exceptions (unwinding, sending metadata through a side channel, etc.) but I think a lot of people find that full exceptions are very useful in the general case and naked "catch" satisfies the YOLO subset case. Plus, bubbling enables you to quarantine the result types to non-error code.[0]: "?" has to know there's a Result type, and which fields are the good/bad fields--it's odd for an operator to know anything about complex, generic types defined at compile time, let alone rely on their semantics.
Hooray! :)
> Whaaaaaa? I think you still have them because you wrote:
Yeah, but keep in mind that this feature is "in development". I'm increasingly just not even using the pattern-matching "pull out :else" property of `if` at all, and while functions return Result types, they're nearly always immediately done away with in some form. I can't think of an occasion in the compiler (my biggest project!) where I have a function returning an optional value and holding on to it as optional.
And yep, it's pretty equivalent to exceptions. The behavior flows like an implicit try/catch. The difference is mostly that exceptions pull in a lot of expensive compiler machinery, whereas breakelse is really just a local branch - and if you tried to ignore it, you'd get type errors, whereas exceptions (unless you have checked exceptions) "fail silent" at compiletime. So the idea of :else return values is that the function is telling you: "There's also a chance I'm not giving you a return value, but something else; you should probably handle this case somehow." I could make operators that propagate this info, but I think it's valuable right where it is, ie. directly after the function call. And there you should either place a marker (?) indicating that you're trusting the compiler to get rid of this state in some way: either via if branch or via a local `else` block, as in the common `foo()? else die`, or explicitly get rid of it in some other way. But going forward from `?`, the matter should be handled; we should avoid carrying it with us any farther than we have to. So in that way it is very exception-like.
> Sort of - I'm specifically avoiding/getting rid of result types.
So yeah, that's what I mean with that. I do have result types in the language, but the point is that I don't propagate them but try to dissolve them locally. The appropriate reaction to an operation yielding a missing optional value should usually be to abandon the current scope right there and then, in some way, not to "will they won't they" with an optional type for another five steps before deciding that "actually, I don't want to do anything with this because it is indeed missing." It's broadly equivalent to typical result/monad based idioms in code, I just think it's a different way of thinking about it that's more suited for the C-like view.
This feels like a good compromise to me actually. It doesn't pull in all the baggage of other possible implementations/mental models (exceptions, result types, blah), it's basically just a step up from tuple types, like reifying a given return value as an error that needs to be handled, which is probably at least the 80% case. Pretty cool.
And allowing `breakelse` in the branch obscures control flow.
Disclaimer: I am a return/break/continue skeptic. I think they obscure control flow too.
https://docs.swift.org/swift-book/documentation/the-swift-pr...
edit: Wait, it's not in the article, I rewrote it and removed that section. My apologies! Anyway, yes, it's like that.
"soatok, of Dhole Moments, is heavily responsible for popularizing the mix of furry and technical content that this article is riffing off of, though the particular format is mostly borrowed from Xe Iaso."
*Socratic dialogue is a discussion between a teacher and his students where both teacher and students ask questions rather teacher be a sage.
**Difference is those sidenotes appear in the main text and have highlighting making them standout.
Anyway, fucking around with formats isn't only fine but also welcome as want to believe are comments regarding whether said formats succeed in making article more enjoyable to read or not. For me guess you can say kinda enjoy Mara, Cadey, and Numa but not really Aoi.
If you find that furry content in writeups bothers you, then you can just not consume content by those authors. If the fact that these keep getting upvoted on HN bothers you, it may indicate that your preferences are not aligned with those of the community you're choosing to participate in.
You can't know that, unless you have a mind reader and can scan someone else's purpose.
> hard to read format
Not everyone finds the same things hard to read, or equally hard.
I'm not saying you're wrong!—I'm saying that this is not the basis for a good HN comment. If everyone who finds something hard-to-read rushes to the comments to complain, the thread will get bogged down in the weeds for sure. You should post when you have something interesting to say.
https://neat-lang.github.io/_images/gurkenglas_idea.png
https://neat-lang.github.io/_images/gurkenglas_looking.png
https://neat-lang.github.io/_images/gurkenglas_neutral.png
https://neat-lang.github.io/_images/gurkenglas_suggesting.pn...
https://neat-lang.github.io/_images/gurkenglas_unimpressed.p...
https://neat-lang.github.io/_images/shoebill_aghast.png
https://neat-lang.github.io/_images/shoebill_considering.png
* Please don't fulminate. Please don't sneer, including at the rest of the community.
* Please don't post shallow dismissals, especially of other people's work. A good critical comment teaches us something.
* Please don't complain about tangential annoyances—e.g. article or website formats, name collisions, or back-button breakage. They're too common to be interesting.
"Please don't pick the most provocative thing in an article or post to complain about in the thread. Find something interesting to respond to instead."