9 ms·
Breakelse: When Compiler Developers Get Bored
- armchairhacker 3y agoThis reminds me a lot of `goto`...
- FeepingCreature 3y ago(Author) I mean, any nonlocal control flow is a form of goto. Break, continue directly; but loops as well. I put breakelse in the structured-flow category, because like break and continue, you can't use it to enter scopes, only exit them.
- quickthrower2 3y agoProbably not as you cant go to an arbitrary location especially a different call stack
- noobermin 3y agobut why not just use goto actually? You also cannot, at least in C++, not jump to an arbitrary location, it must be within the current function.
- Snild 3y agoOne could ask the same thing about `for` and `while`: why not just use `if`s and `goto`s? Because they're convenient and useful constructs. I don't know if `breakelse` is too, but maybe -- especially when part of a `?` operation.
- quickthrower2 3y agosometimes less flexibility is good.
- unnouinceput 3y agoIt is good old "goto" from Basic, but is a specially lobotomized "goto" instead of having the full power of a normal "goto". And the article's author goes like "With a bit of syntax sugar, this gives a novel alternative to conditional-access operators.". Novel? For what year? 1970? Definitely not for 2023
- avgcorrection 3y ago> Novel? For what year? 1970? Definitely not for 2023 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.
- FeepingCreature 3y agoI realize this is a bit passe, but there were reasonable arguments brought back then about goto and its harmfulness, that I personally find very convincing. :P Do you have precedent for something like breakelse as an explicit operation?
- hirako2000 3y agoCalling it lobotomized goto is not misplaced but associating an unethical "medical practice" is very deragagory and totally unjustified towards breakelse. 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
- teo_zero 3y agoBut goto is a statement. What TFA advocates for is 1) an expression that can be used whenever expressions are expected, in particular inside a .case(), and then 2) a bit of syntactic sugar to abbreviate the verbose phrase .case(:else:breakelse) into a convenient one-character symbol.
- al_al 3y agoIsn't this like a `throw`, where the else is the catch/finally?
- themoonisachees 3y agoIt seems so, but the catch being restricted to the else block of this exact if block means it's considerably less expensive than a regular exception. additionally, most of the time when doing operations covered by this, you wouldn't really care about the type of your supposed exception (ie why bother throwing a endoffileexception when you know you're going to catch it right there), so for this use case specifically this gives you a free pass to say "go to error handling, i don't care about naming things because this is all extremely local"
- FeepingCreature 3y ago(Author) Yep, it's sort of like throw/catch at the expression level, but through a different mechanism that doesn't allow non-local unrolling. In exchange, it's a lot more performant.
- a1o 3y agoI like I don't have to add names for things and it's perfectly easy to follow, plus I still get things that happen when going out of scope that wouldn't happen if I used a go-to, I found it brilliant! Great work!
- 22c 3y agoI found the blog post a bit hard to follow but there doesn't seem to be a need for exceptions here. 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).
- FeepingCreature 3y agoExactly! 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.
- paxcoder 3y agoRestricted to `.case(:else: breakelse))` in the conditional, there seems to be no advantage to this over optional chaining. And allowing `breakelse` in the branch obscures control flow. Disclaimer: I am a return/break/continue skeptic. I think they obscure control flow too.
- FeepingCreature 3y ago(Author) The advantage over chaining, at a compiler level, is that (absent compiler optimizations) you still save one branch. With chaining, you end up with a Maybe/Optional/whatever at the end of the expression, that `if` then has to unwrap separately. `breakelse` jumps directly to the else branch. That also makes it a bit easier to understand in the success case, imo, because you get rid of the error types immediately, you don't wrap them into a final outer optional type.
- paxcoder 3y agoOptional binding can refine the type. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/thebasics/#Optional-Binding https://docs.swift.org/swift-book/documentation/the-swift-pr...
- FeepingCreature 3y agoYes, as I mentioned, it's like an `if` with multiple bindings, but I don't force intermediate variable names. That example is in the article. :P edit: Wait, it's not in the article, I rewrote it and removed that section. My apologies! Anyway, yes, it's like that.
- paxcoder 3y agoI'm not sure I understand what you mean by multiple bindings? With if-let, binding to the variable is conditional, and it only happens if all parts of the chain return an actual value. Otherwise, as soon as one returns null, a jump can be made to the alternative branch. Am I missing something?
- BoppreH 3y agoHey, I have something similar in my (unpublished) language, even with the same character. However, the operator can appear anywhere within the body: 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.
- FeepingCreature 3y ago`?` can do that in Neat, but you'd use a different mechanism where you mark some members of a return value as `fail` fields, which would then be error-propagated out of the entire function by `?`. Ie. `get_user_id` would return `(UserId | fail HttpError)`, and `get_user_id()?` would get you an expression that either yields `UserId` or returns `HttpError`. Then you could use a nested function to imitate your `try` block, or just map `HttpError` to whatever error you want with `case`. 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.
- light_hue_1 3y agoYou can translate this directly to Haskell and it works as you would expect. You also have a lot of freedom on how to process the answer. 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 :)
- 3y ago
- mjw1007 3y agoTheir motivating example is dealing with a function that needs to both indicate success/failure and return a value on success. 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%29 https://en.wikipedia.org/wiki/Icon_%28programming_language%2...
- layer8 3y agoEspecially for failure (as opposed to just optionality), I think that exceptions are the right approach, because in general you want to transport data and type about the failure. That doesn’t imply the traditional try-catch-finally syntax — I think we can find better syntax for typical cases — but having a distinction between a normal return (type) and abnormal returns (return types). This is like a special kind of sum type where one of the options is specially distinguished.
- JonChesterfield 3y agoExceptions are the wrong thing. 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.
- layer8 3y agoWith checked exceptions, there is no difference from sum types in terms of interface contract and type checking. You’re not throwing anything away. The exception carries a stacktrace that tells you exactly where the failure was detected. The exception type and the data that the exception carries can give you all the data you might want about the failure. Exceptions can be chained (wrapped) to give additional context and/or to conform to an up-call-path interface contract, without losing any of the original information. Secondary failures during stack unwinding can be attached to the original exception (as for example Java does automatically in try-with-resources for resource cleanup), so in general you get a tree of errors that corresponds to the failures encountered in the call tree. This gives you much more information than the typical result codes/enums. The runtime cost is proportional to the cost you already had in that call tree.
- victorNicollet 3y agoC# has boolean pattern-matching (and library functions with TryXYZ names) as a solution to this kind of issue. For instance: 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.
- camgunz 3y agoBreakelse 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.
- rdtsc 3y agoThat’s very neat. I appreciate the conversational style. Both audience members seem to have different backgrounds so it’s interesting to see it from their perspective. The feature itself looks the same as Erlang’s maybe expression: https://www.erlang.org/blog/my-otp-25-highlights/#selectable-features-and-the-new-maybe_expr-feature https://www.erlang.org/blog/my-otp-25-highlights/#selectable.... I wonder what other languages have this.
- jshshxheg 3y agoWhat about a finally block instead? a la try, catch finally.
- Const-me 3y agoNot a big fan. Consider that: 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.
- FeepingCreature 3y agoThat's true. However, you would rarely use nested-else blocks like that to begin with. The whole point is that you can avoid deep nested scopes.
- narnarpapadaddy 3y agoWould it be possible to restrict the use of breakelse only to if statements that contain an else?
- FeepingCreature 3y agoSure, and I agree it's kind of misleading, but half my motivation is to skip past an optional if block. I think of it as 'else' being there, but empty.
- jeroenhd 3y agoThis is a good point. I think it can be made more readable by forcing labeled else statements, like: '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.
- boxed 3y agoMy Swedish brain breaks from that word. It looks like "bakelse" which means pastry.
- whoopsie 3y agoIn Swift this is more sanely and maintainably handled without a breakelse anonymous goto ``` do { try thowingMethod() } catch { // interrogate error } ```
- Kye 3y agoAnimals make everything better.
- dang 3y ago[stub for offtopicness]
- amelius 3y agoMaybe if the author got rid of the furry creatures, he could get a clear view of things.
- CaptainFever 3y agoHey, don't diss the furries in tech :P
- yjftsjthsd-h 3y agoThey seem like a perfectly reasonable expository device to me.
- lloydatkinson 3y agoAnother strange blog where people use animals as some kind of conversation intermixed with normal prose. Very confusing.
- pests 3y agoIt is deliberate - "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."
- IshKebab 3y agoStill weird though. And ridiculously verbose.
- lifthrasiir 3y agoNot because of furry though.
- drpixie 3y agoAm I the only person who thinks the whole thing is a joke, a bit like brainfuck - https://en.wikipedia.org/wiki/Brainfuck https://en.wikipedia.org/wiki/Brainfuck
- FragenAntworten 3y agoI like this. It seems similar to `if let Some(value) = option` in Rust, with `?` replacing the pattern matching and supporting multiple assignments. 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.)