(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.
It 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
> 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.
I 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?
Calling 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
But 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.
It 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"
(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.
I 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!
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.
Yep, in an expression language that's how you'd do it. However, we're an imperative language, and the advantage of `else` is that we can use an imperative construct, `if()`, to locate the else case in the code - so we don't need to handle it in the typesystem. That's an inversion from monadic languages, where types contain/emulate statements - in C-likes, statements contain expressions/types! So `breakelse` effectively is glue that lets us turn part of the type into a control flow, which is deliberately not represented in the typesystem. `Optional.case(:else: breakelse).X` and now in X, we're no longer dealing with an `Optional` at all. We're not "inside" the Optional as we would be with `map`; we've effectively everted the entire `Optional` using non-local flow from the `else` case.
(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.
I'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?
Hey, 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?
}
`?` 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.
Especially 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.
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.
With 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.
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.
Yep, 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.
> 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".
Sort of - I'm specifically avoiding/getting rid of result types. Usually with null-coalescing ? you'd go "optional ?. optional ?. field" => optional. But because '?' acts as a control flow primitive (via `breakelse`), it's actually "optional? .optional? .field" => field, ie. each optionality is done away with by branching away, and so no longer represented in the type system at all.
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'm hoping my tone is getting this across but, I like geeking out about PLs so I'm going whole hog haha. I like this language a lot FWIW; feels pretty advanced and very thoughtful :)
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.
> (I'm hoping my tone is getting this across but, I like geeking out about PLs so I'm going whole hog haha. I like this language a lot FWIW; feels pretty advanced and very thoughtful :)
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.
Yeah that's the other thing: Results melt away more or less so it's kind of a big todo about nothing. Promises I understand passing around but like, feels like a lot of machinery (generics, sum types, pattern matching) just to let a function say "whoops".
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.
That’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.
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.
Sure, 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.
This 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.
In Swift this is more sanely and maintainably handled without a breakelse anonymous goto
```
do {
try thowingMethod()
} catch {
// interrogate error
}
```
"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."
What soatok is doing is basically similar to what many other blogs have been doing throwing random images/memes in the text. (Also wouldn't call the furry part any popular.) At least Xe's format has some logic[0] for its introduction. Albeit usage has been less of Socratic dialogue* and more like sidenotes**. Submitted article is an attempt to original concept but fails to it.
*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.
Xe here. For what it's worth, I basically combined what Soatok does with the furry stickers and what fasterthanlime does with Cool Bear. This pseudonarrative style has a lot of promise for making things less gatekeepy in our industry and I can't see why encouraging people to fuck around with their formats in this way is a bad thing.
From fasterthanlime's only checked at start of linked article and saw it included some useful info, such as article being old and required rust version, so thought they used tips to do some snarky comments and provide info not belonging to main text but simultaneously important. Reading to end can see they also employ some pseudonarrative (actually a nice name for this style).
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.
Yeah, that’s exactly the one I was thinking about when I wrote that. The normalisation of furry content as part of technical content is not a trend I’m looking forward to.
More and more, people are bringing their whole selves to what they do, and how they present their own work. In my personal opinion, this is a good thing.
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.
It's fine to dislike things! In this case, while I am riffing on Xe's design, because of how I wrote the article the people involved are, you know, actual other people, so it was genuinely useful to have an immediately visible shorthand for who was talking and what I thought their sentiment was. Using IRL photos for that would be for one impractical; for another, it's easier to make sentiments stand out on cartoons. (Not that SD was very good at this here.)
The mix isn’t particularly confusing, but I find this kind of socratic dialogue quite annoying in general. It just adds cognitive overhead, because besides the author’s train of ideas I now also have to deal with the presentation as different characters and their way of commenting and interjecting and asking questions, all of which are immaterial to the actual ideas and arguments. In the present case, it also makes an already lengthy discussion even more laborious.
"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."
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.
Hi! You seem to be fulminating about a tangential annoyance. Take a look at the guidelines (https://news.ycombinator.com/newsguidelines.html), so you can get up to speed on the community and become a more productive member for it.
I'm reluctant to be a hall monitor here, but to everyone annoyed about "the animals" pick any one of these guidelines [0] you're violating and maybe think about not doing it anymore:
* 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."
78 comments
[ 3.5 ms ] story [ 150 ms ] threadBecause 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
This seems more like
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:
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
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.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.
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.
I am aware of Monads (they're explicitly called out in the article!), and specifically want to go in a different direction.
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.
---
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:
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:
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.:
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.
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.
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.
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:
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."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.
[0]: https://xeiaso.net/blog/how-mara-works-2020-09-30/
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.
https://news.ycombinator.com/newsguidelines.html
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://news.ycombinator.com/newsguidelines.html
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
https://neat-lang.github.io/_images/shoebill_neutral.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.
[0]: https://news.ycombinator.com/newsguidelines.html
"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."
Rather than something like this:
you'd have something like this: (Pseudocode for both examples.)