4 ms·
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 complete
by FeepingCreature 3y ago
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.
- 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".
- FeepingCreature 3y agoSort 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.
- camgunz 3y ago(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.