Stream: ideas

Topic: `if` condition pattern match


view this post on Zulip Richard Feldman (Feb 27 2025 at 20:26):

I realized this scenario is currently kind of awkward in Roc syntax:

match paths.first() {
    Ok(path) => File.delete!(path)?
    _ => {}
}

basically the situation where you want to pattern match on a tag union, extract a named value from it, and then run an effect that ends up with you having {} - and then otherwise doing nothing

view this post on Zulip Richard Feldman (Feb 27 2025 at 20:27):

what if we allowed this instead?

if Ok(path) = paths.first() {
    File.delete!(path)?
}

view this post on Zulip Richard Feldman (Feb 27 2025 at 20:27):

(this is essentially if let in Rust but without the let)

view this post on Zulip Isaac Van Doren (Feb 27 2025 at 20:49):

Sounds great! I don’t like that it increases the surface area of the language, but it seems well worth it.

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 20:54):

As long as we don't add matches!

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 20:55):

I really hate that some people use if let and other use if matches

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 20:55):

Hmm

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 20:55):

Actually, I don't like the syntax. Too easy to make a bug.

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 20:56):

Richard Feldman said:

what if we allowed this instead?

if Ok(path) = paths.first() {
    File.delete!(path)?
}

That might be a bug....the user might actually want:

if Ok(path) == paths.first() {
    File.delete!(path)?
}

view this post on Zulip Sam Mohr (Feb 27 2025 at 21:14):

That should be caught by shadowing warnings

view this post on Zulip Sven van Caem (Feb 27 2025 at 21:14):

I wonder if this has been brought up before: https://cse.hkust.edu.hk/~parreaux/papers/ultimate-conditional-syntax-ml22/

It does propose to combine pattern matching and conditionals which has been suggested and rejected many times here, but it would solve this particular issue rather nicely without introducing a third bespoke control flow construct to the language

view this post on Zulip Sam Mohr (Feb 27 2025 at 21:14):

If they put a single equals sign, then sign can't already be defined

view this post on Zulip Sam Mohr (Feb 27 2025 at 21:15):

If they put double equals, it's already defined

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 21:18):

Sam Mohr said:

That should be caught by shadowing warnings

Unless the variable has an _ after it. Or if a new user is confused and just assumes they need to add an underscore after to remove the warning

view this post on Zulip Sam Mohr (Feb 27 2025 at 21:23):

Yeah, well

view this post on Zulip Sam Mohr (Feb 27 2025 at 21:26):

I think that's a somewhat minor case, and I think newcomers that don't add the underscore initially will read the warning that said "add an underscore to make it reassignable"

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:05):

I think it would always be a compile error if you meant to use ==

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:08):

Probably won't be a common error, but I bet there will be users that go from:

path = "..."
if Ok(path) = paths.first() {
    ...
}

To

path_ = "..."
if Ok(path_) = paths.first() {
    ...
}

Which is still broken. Feels like a really easy thing for a new user to accidentally do

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:09):

really? :thinking:

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:09):

why would someone try to write that?

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:10):

I can see mixing up = and == (and getting a compile error) but I don't follow why someone would attempt to do that, even by mistake :big_smile:

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:10):

oh wait I understand

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:11):

you're saying they wrote Ok(path) = when they should have written Ok(path) == in that last example

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:11):

but we can do a more specific error if they do shadowing inside an if pattern

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:12):

like "hey are you sure you didn't mean to do == here?"

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:13):

also note that the more likely beginner mistake would be the opposite order: if list.first() = Ok(path) just because that's the order people usually write things like that in

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:14):

and that would obviously be an error (which also could give a custom helpful message)

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:17):

Good point

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:20):

Also, would this allow for any sort of pattern matching?

if [x, y, z] = my_list {
    ...
}

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:22):

yeah totally

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:22):

as long as it's exhaustive

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:22):

actually nm wouldn't need to be in that case I guess

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:22):

since it's if and not match

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:40):

If we support this, I think we should maybe not support ?? return val

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:41):

Since:

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:45):

I definitely think we should support both

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:45):

It is really the same as regular ? to me

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:45):

Like even with pattern matching if we will still want ?

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 22:46):

But it is technically redundant

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:46):

Maybe ?? using question marks is confusing, then, since they both handle Results but very differently IMO

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:46):

? does early return unconditionally

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:47):

?? does no early return, unless the thing passed to it returns in its expression

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:48):

I think the reason we even suggested allowing ?? to support return was because we had suggested some pre-braces version of if-let that got rejected, so the ?? return val was a simple stopgap that worked for Result and no other union types

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:49):

Actually, I may be wrong

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:50):

The x = y ?? return val desugars to:

x = match y {
    Ok(ok) -> y
    Err(_) -> return val
}

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:50):

Which would need the equivalent of let-else, not if-let

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:50):

yeah

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:50):

if wouldn't be convenient for that use case

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:50):

AKA

Ok(x) = y else {
    return 5;
}

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:51):

I guess if we want return x to be an expr anyway, then so long as we don't support let-else then we still maintain basically one way to do things

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:52):

So I think ?? return val is okay, and we probably shouldn't add let-else if it stays

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:53):

But I think let-else is more communicative, and ?? doesn't work for non-Results, not true for let-else

view this post on Zulip Richard Feldman (Feb 27 2025 at 22:53):

yeah I'm not a fan of else without if

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:58):

We already have if sans else, I'm not seeing why this is much different

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:59):

The other thing we brought up last time was that the above example makes its control flow known at the end of the line

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:59):

But the same problem applies to ?? return val

view this post on Zulip Sam Mohr (Feb 27 2025 at 22:59):

It follows an expression with no preface

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:02):

So if we need to pick a different word, that's okay with me. But ?? return not handling non-Results and not actually providing a default value for a Result is a strong pair of detriments in my eyes that don't exist in let-elsecatch|orelse|fail|fall|etc.

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:04):

we already have foo()? with control flow at the end of the line

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:04):

Sam Mohr said:

We already have if sans else, I'm not seeing why this is much different

you mean not seeing why else sans if is different?

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 23:05):

Sam Mohr said:

We already have if sans else

We do?

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:10):

Richard Feldman said:

we already have foo()? with control flow at the end of the line

Yes, which is why I think there's no advantage in this aspect for ?? vs. let-else

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:12):

Brendan Hansknecht said:

Sam Mohr said:

We already have if sans else

We do?

Two places, on already agreed on, one planned but not fleshed out:

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 23:12):

Oh, I read that backwards

view this post on Zulip Brendan Hansknecht (Feb 27 2025 at 23:12):

I was thinking else sans if

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:13):

Yep, that doesn't exist elsewhere

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:13):

I proposed it for the behavior that Richard pushed should instead be what the ? binop is today

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:13):

Aka try-else in this proposal I wrote a while back: https://docs.google.com/document/d/1pBNytZYF5aOCYgmHno2Y-8im-tWfbLLYN5RIu8dEe6s/edit?usp=sharing

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:14):

So Richard has now made his if-less else dislike known twice

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:15):

But I'm still making known my thought that it seems to work pretty well in Rust and have fewer downsides than ?? return

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:32):

I don't understand the downside of ?? return

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:32):

it isn't a special-case haha

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:32):

it's just a natural consequence of what ?? desugars to

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:32):

we'd have to create a special rule to disallow it if we didn't want it to exist, right?

view this post on Zulip Richard Feldman (Feb 27 2025 at 23:32):

because it's ?? <expr> and return ____ is an expr

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:35):

I think it's because I'm thinking long-term, we don't want error messages for ?? to say "this match statement has problems"

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:36):

Meaning I'd want to "desugar" it post-typechecking, as we were doing in the Rust-based compiler for the ? suffix

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:38):

We want to be able to say "this thing should be a Result" for values before ? or ?? for max friendliness

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:39):

And so, in the non-desugaring world, I think the special-case isn't there

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:40):

I guess we already expect people to consider the desugared code when writing binops since that's somewhat required to understand how + and - interact with methods

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:41):

But still, I think expecting users to think about how something desugars is smelly to me

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:42):

It's no longer "use ?? to provide lazy defaults for Results", it's "use ?? as a desugaring to a match expression that handles Results"

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:43):

It's hard for me to understand what's best for the average developer here, since I know how all this stuff works, having worked on this part of the compiler a good deal

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:43):

All in all, I definitely think ?? return works

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:44):

It just goes weakly against the "small set of orthogonal primitives" cardinal guiding rule

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:45):

Since you can't do Admin(admin) = user ?? return NotAdmin

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:45):

You need to now break out a whole match statement

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:47):

But with some let-else equivalent, you get Result and non-Result handling with the same feature

view this post on Zulip Sam Mohr (Feb 27 2025 at 23:48):

everything Just Works:tm:

view this post on Zulip Richard Feldman (Feb 28 2025 at 00:07):

hm, I don't follow

view this post on Zulip Richard Feldman (Feb 28 2025 at 00:07):

I totally get the point about the compiler wanting to special-case it in order to give more helpful error messages, but I think that's good compiler design and not part of the rules of the language

view this post on Zulip Richard Feldman (Feb 28 2025 at 00:07):

the "small set of simple primitives" goal is about the rules of the language

view this post on Zulip Richard Feldman (Feb 28 2025 at 00:08):

and that's the part where ?? <expr> implies that ?? return ____ should work

view this post on Zulip Richard Feldman (Feb 28 2025 at 00:08):

and having one more (of many others!) compiler special-cases to improve error message quality is unrelated to that

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:22):

Yeahhhhhh... I think even with the compiler not desugaring ?? to a match and instead making it a proper IR node, we'd have to actively disallow the use of return there if return is an expression

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:23):

So if return is an expression, my point is moot

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:23):

I think that return only would make sense as an expression and not a statement if we need it for ??, right?

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:24):

I think supporting val.method(arg1, return other) would be a weird side effect

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:25):

So if it's an expression and not a statement, we'd have to give warnings anywhere it's used within another expression

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:25):

Except for ?? and brace-less if

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:25):

But brace-less if is already a warning that we format away

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:27):

So now, talking through this, it seems like we are supporting "return as an expression" to support ?? and actively warning about "return expression" used elsewhere, but pushing against other solutions because we have "return as an expression" already

view this post on Zulip Sam Mohr (Feb 28 2025 at 00:28):

So if there's another place we need "return as an expression", then again I can shut my trap

view this post on Zulip Sam Mohr (Feb 28 2025 at 01:12):

@Sven van Caem just letting you know I've looked at the paper you linked on UCS. It's an interesting concept that I think could marry well with our plans for union refinement, but I'm not sure how understandable this would be to beginners since UCS doesn't "read like English" in the way if-else and when-is do... I'll have to mull this over

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 01:21):

Return expressions likely could be used in some if or match expressions that not only early return but also return a value in other cases.

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 01:22):

So I don't think it is ?? specific

view this post on Zulip Richard Feldman (Feb 28 2025 at 01:24):

return has to be an expression for this to work:

answer = if a { do_something(b) } else { return "blah" }

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 01:24):

:point_up: yeah, exactly that

view this post on Zulip Richard Feldman (Feb 28 2025 at 01:24):

same with having it in match arms

view this post on Zulip Richard Feldman (Feb 28 2025 at 01:25):

because an expression is the thing that comes after else

view this post on Zulip Anthony Bullard (Feb 28 2025 at 02:13):

Technically it doesn’t have to be an Expr in that example if we say that returns can end a block but then there isn’t much difference

view this post on Zulip Anthony Bullard (Feb 28 2025 at 02:15):

But return in many many many Expr positions is ridiculous, but I guess that is fine for the purposes of parsing

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 02:22):

Recently I read Vine docs, found a bit awkward yet interesting take on the related syntax (also the implication operator):

https://vine.dev/docs/features/conditions#the-is-operator

Speaking of roc,

Does this look weird?

if paths.first() is Ok(path) {
    File.delete!(path)?
}

Does this look even weirder?

if paths.first() is not Ok(_) {
    return 42
}

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 03:24):

Btw why x ?? y and not x.with_default(y)?

I guess x ?? y was introduced because x |> Result.with_default y was noisy and inconvenient. But with static dispatch it doesn't look that bad, right? It's even more expressive imo

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 03:28):

Kiryl Dziamura said:

Btw why x ?? y and not x.with_default(y)?

This is a really solid question now that we have static dispatch

view this post on Zulip Richard Feldman (Feb 28 2025 at 03:37):

it's just much more concise haha

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 03:53):

Sure, but with default really isn't that common. So is it special enough to deserve ??

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 03:54):

It isn't uncommon, but I don't think it is particularly special. Like if we had static dispatch first we might not have ever considered adding ??

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:00):

:thinking: I wonder if there's a more concise name that would close the gap

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:02):

Also, it's likely you won't use ?? in chaining because it would require parens around the expression. So sometimes you would fallback to the chaining syntax. It makes ?? convenient only in specific cases

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:05):

x ?? y
x.ok_or(y)
x.if_err(y)
x.or_else(y)
x.with_default(y)

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:08):

ok_or is pretty close

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:09):

x.or(Ok(y))? -> x.or_ok(y)? :big_smile:

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:13):

or is a reserved keyword at the moment

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:13):

Speaking of naming. I hate the word “unwrap” but at least “unwrap_or” expresses what happens with the value in opposite to “with_default”. Just a note

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:14):

or is reserved

Yeah, I was joking. That kind of syntax would mess with the return type anyway

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:29):

so I guess the three options would be:

path = paths.first() ?? return ""

Ok(path) = paths.first() else return ""

if Ok(path) = paths.first() {
    ...
}

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:32):

aesthetically I still don't like using else like this, but if we were to get rid of ??, it does have the objective advantage of being more flexible because it works on non-Result patterns too

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:38):

Kiryl Dziamura said:

Recently I read Vine docs, found a bit awkward yet interesting take on the related syntax (also the implication operator):

https://vine.dev/docs/features/conditions#the-is-operator

Speaking of roc,

Does this look weird?

if paths.first() is Ok(path) {
    File.delete!(path)?
}

Does this look even weirder?

if paths.first() is not Ok(_) {
    return 42
}

I’m still wondering what do you guys think about this :grinning_face_with_smiling_eyes:

It's not ideal, but it eliminates both the = vs == problem and let else

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 04:41):

I’m not sure how often negative partial matching is needed tho

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:57):

I think that's more confusing than = vs ==

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:57):

because a lot of languages use is as an expression

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:57):

in some cases it even means the same thing as ==

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:57):

so in those languages, if paths.first() is Ok(path) { would mean the same thing as if paths.first() == Ok(path) {

view this post on Zulip Richard Feldman (Feb 28 2025 at 04:57):

I don't know of any languages that have = and == doing the same thing :big_smile:

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 05:14):

Oh, now I see the intention of the first two options:

path = paths.first() ?? return ""

Ok(path) = paths.first() else return ""

I don't like them because they unexpectedly mix two statements, but they don't increase the indentation.

view this post on Zulip Richard Feldman (Feb 28 2025 at 05:15):

how would you prefer to write this?

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 05:16):

I definitely don't like how this reads:

Ok(path) = paths.first() else return ""

I definitely read it as:

Ok(path) = (paths.first() else return "")

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 05:27):

Can ?? be used only for the early return? The question marks suggests it in some sense. It might be implied as parametric unwrap where both the expression and the return value are not necessarily Results.

When you don't need early return - use .with_default

Ok(path) = paths.first() ?? "" # return empty string if not Ok

# desugars to
match paths.first() {
    Ok(path) => # continuation
    _ => ""
}

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 05:31):

It would work only with incomplete pattern matching: pat = expr ?? expr.

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 05:47):

However, early return for the operator is likely uncommon in other languages. Maybe other options that involve a question mark?

view this post on Zulip Sam Mohr (Feb 28 2025 at 06:07):

We could support let-else with

unless Admin(admin) = user {
    return fail
}

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 06:23):

I don't expect it's needed often so the verbosity is not a problem. Also I like how it complements the if case

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 06:37):

if !(Admin(admin) = user) {
    return fail
}

view this post on Zulip Sam Mohr (Feb 28 2025 at 06:37):

Hmmm

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 06:38):

That probably looks a bit confusing, but unless feels quite unnecessary to me

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 06:38):

I feel like something more built into if would make more senses here.

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 06:38):

Oh, though scoping is different than if.....hmm

view this post on Zulip Sam Mohr (Feb 28 2025 at 06:38):

Its stolen from Ruby

view this post on Zulip Brendan Hansknecht (Feb 28 2025 at 06:39):

ah

view this post on Zulip Artur Domurad (Feb 28 2025 at 08:59):

The problem with:

if !(Admin(admin) = user) {
    return fail
}

and

unless Admin(admin) = user {
    return fail
}

Is that most often you would want to early return if the condition is not met, but continue and use the unwrapped value if the condition is met.
Looking at this, it doesn't seem like the admin should be in scope after the if/unless block.
But if admin is available only in the body of the if then it is useless, because you enter the block only when the condition is not met, and so admin would not be bound to anything.

view this post on Zulip Artur Domurad (Feb 28 2025 at 09:09):

Aaaa, so with unless the admin binding would be available after the block.
Seems confusing to me, but I never used ruby...

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 09:12):

yes, it kinda reminds the mind bending around backpassing. but I like the unless proposal anyway :blush:

view this post on Zulip Sam Mohr (Feb 28 2025 at 10:18):

I'd expect the admin variable to be in scope for the rest of the code following the closing brace

view this post on Zulip Richard Feldman (Feb 28 2025 at 12:03):

Kiryl Dziamura said:

Can ?? be used only for the early return? The question marks suggests it in some sense. It might be implied as parametric unwrap where both the expression and the return value are not necessarily Results.

When you don't need early return - use .with_default

Ok(path) = paths.first() ?? "" # return empty string if not Ok

# desugars to
match paths.first() {
    Ok(path) => # continuation
    _ => ""
}

I think at that point I'd probably expect it to just work like infix ? does today except without the lambda, so both of these would do the same thing:

path = paths.first() ? |_| ""

path = paths.first() ?? ""

view this post on Zulip Richard Feldman (Feb 28 2025 at 13:47):

in that world, the scenarios are:

view this post on Zulip Gábor Librecz (Feb 28 2025 at 14:00):

I'm just lurking here, but I like the idea of something like unless. Its similar to Swift's guard statement: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/statements/#Guard-Statement

view this post on Zulip hchac (Feb 28 2025 at 14:10):

Kiryl Dziamura said:

Oh, now I see the intention of the first two options:

path = paths.first() ?? return ""

Ok(path) = paths.first() else return ""

I don't like them because they unexpectedly mix two statements, but they don't increase the indentation.

I'd like to +1 this: it would be nice to avoid additional indentation (as long as it meshes well with the rest of the language)

If I'm following correctly, with the if Ok like Rust we would get code that goes from something like:

first = List.first(vals) ?? return 123
op = List.first(ops) ?? return first
... perform work with first and op ...

to

if Ok(first) = paths.first() {
    if Ok(op) = ops.first {
        ... perform work with first and op ...
    } else {
        return first
    }
} else {
    return 123
}

or the equivalent with when/match:

when vals is
    [] -> 123
    [first, .. as restvals] ->
        when ops is
            [] -> first
            [op, .. as restops] ->
                ... perform work with first and op ...

I'm certainly not pushing for early return after ?? if it doesn't mesh well with the language, just would like a construct that gives me the equivalent power to avoid indentation.

view this post on Zulip hchac (Feb 28 2025 at 14:20):

Though of course there's always this, which avoids additional indentation:

        first = if List.len(vals) == 0 then
            return 123
        else
            List.first(vals) ?? crash "expected non-empty list"

        ...
        # continue using first

But it feels off having to check length first, then calling first() with returns a Result and having to either ? which forces the outer function to return a Result (not ideal), or like in this snippet, "forcing" a crash even though it shouldn't be possible.

Maybe some form of flow-analysis avoids the Result from List.first, but that sounds complicated.

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:23):

one of my learnings from Ruby is not to have an unless

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:24):

having two ways to do if bothers a lot of people

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:24):

e.g. you get into situations like someone writes an unless and then later comes in and wants to add an else, but then it's like "should it be unless...else? Or now do we switch it around to if...else?"

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:29):

Kiryl Dziamura said:

Kiryl Dziamura said:

Recently I read Vine docs, found a bit awkward yet interesting take on the related syntax (also the implication operator):

https://vine.dev/docs/features/conditions#the-is-operator

Speaking of roc,

Does this look weird?

if paths.first() is Ok(path) {
    File.delete!(path)?
}

Does this look even weirder?

if paths.first() is not Ok(_) {
    return 42
}

I’m still wondering what do you guys think about this :grinning_face_with_smiling_eyes:

It's not ideal, but it eliminates both the = vs == problem and let else

I don't think the lack of negative pattern matching is a problem, since Roc doesn't have one right now. Also, when you need that, seems like a guard pattern match on the negative case (like the one proposed here) solves the problem.

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:29):

When I was learning Rust, I found if-let pretty unintuitive. Especially without capturing (the programmer should have used matches! in that case, but – sharing Brendan's view –, I hope in Roc, we plan to support one way of doing this). I understood the concept, but the whole syntax seemed to be in reverse. At least the let notified me that i'm dealing with a pattern match disguising itself as an assignment.

// Okay, i guess we are assigning to _, which is ignored
if Err(_) = my_result {
  return 3
}

// This just seems wrong, despite being a completely valid use case,
// since there is no assignment here at all
if Red = color {
  return 3
}

I like the is syntax. I'm not a newcommer to pattern matching, so I might just not see why is could be confusing, since I immediately think that inside the patter, there is a capture, not a variable that will be substituted and checked for equality.
The when a is ... expression used is as well, and I always found that elegant.

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:30):

I also forgot to note that currently ?? return "..." can also be used as ?? crash "..." e.g. in short scripts, which is handy

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:33):

the problem with is is that, for example, a valid Python expression is (a is b) just like how (a == b) is a valid Python expression

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:33):

But you have taught people Rust Richard. Did you find beginners be confused with if let? If not, my concerns aren't as relevant and I'm left with a preference for the style.

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:33):

I haven't taught if let to beginners

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:34):

but personally, I don't like how if let reads in Rust, but I'm (for whatever reason) fine with if Ok(path) = paths.first() {

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:35):

it might be because if let is just a strange thing to read on its own

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:35):

like if takes a condition, and let isn't a condition, it's a statement

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:35):

so it's strange to see if followed immediately by let

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:35):

whereas in if Ok(path) = paths.first() it's if followed by a pattern, which is already a form of condition in a match

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:36):

oh, another idea I just thought of:

if Ok(path) => paths.first() {

}

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:36):

so reuse the same => from match

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:36):

Yeah, makes sense you didn't teach that. Well, I get the concern with the python example. Maybe error messages would help there saying that a pattern was expected there.

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:36):

my concern with is is less on the writing and more on the reading

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:37):

Yes, if and let together was confusing as a beginner.

view this post on Zulip Richard Feldman (Feb 28 2025 at 16:37):

like you read it and you are confident (but incorrect) that you understand what it's doing, based on habit from other languages

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:38):

That's true.

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 16:41):

Richard Feldman said:

oh, another idea I just thought of:

if Ok(path) => paths.first() {

}

I think the arrow doesn't fit here. Makes me think that Ok(path) stands on it's own and after evaluation, comes the paths.first() part, since that's the direction.

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 21:24):

A breakdown of how I got to one of the already proposed ideas. Also some thoughts on related syntax (if you don’t care - just jump to the last two points):

unwrap res or return

res?

Just a good old question postfix operator. Use to early return whatever error with convenient chaining

unwrap res or map error and return

res.map_err(|_| -> "")?

Use to early return mapped error with convenient chaining.

I'm not a fan of infix ?. I don't see how it helps especially considering chaining. Yes, it's short, but this is the only advantage imo

unwrap res or default

res.ok_or(def)

This doesn't allow early return but allows convenient chaining. Use to unwrap with default value

unwrap res or short circuit

res ?? expr

The return statement lives inside of blocks which are expressions (and short circuit). Meaning it should have expr in the "else" branch. Use to either unwrap with default value, or to early return

x = res ?? 42
y = res ?? { return 42 }

But! :point_down:

generic unwrap or short circuit

Ok(x) = res ?? expr

Instead of locking syntax only on the result, it’s possible to extend it to other patterns. In the same fashion, expr is short circuit, early return is possible, and indentation doesn’t suffer. Chaining is not possible. Use to either unwrap with default value, or to early return

Ok(x) = res ?? 42
Ok(y) = res ?? { return 42 }

view this post on Zulip Richard Feldman (Feb 28 2025 at 22:07):

I do like how res ?? { return 42 } reads!

view this post on Zulip Norbert Hajagos (Feb 28 2025 at 22:10):

So for the generic unwrap, ?? would depend on the result of the pattern match, not only on the data, right? Meaning the operator works like this:

(Ok(x) = res) ?? 42
(Ok(y) = res) ?? { return 42 }

It took me a while to realize this (if that is indeed what's happening), since (once again I come back to this point) the assignment makes me think that's how the operator would work:

Ok(x) = (res ?? 42)
Ok(y) = (res ?? { return 42 })

I think it's fine with just the pattern match part, but with the ?? thrown into the mix, it's getting hard to understand. Specifically, I would rather expect this to work

Ok(x) = res ?? Ok(42)

If your plan is to generalize from Results, we would need a way to handle more than 1 args. But at that point, the verbosity starts to subsume the convenience of??.

Coord(x, y) = res ?? Coord(42, 24)

view this post on Zulip Sam Mohr (Feb 28 2025 at 22:12):

The point of something like res ? error_mapper is that I see a lot of blind error propagation in Rust which loses stack context, and I think we should make it as easy as possible to provide that context, which this solution does by only needing one character and two spaces, almost the minimum possible

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:17):

I feel like this conversation has become so sprawling that it's hard to digest what is and is not begin proposed anymore - or what problem(s) we would like to solve.

It feels like we want to overhaul the entire syntax in the language for:

  1. Non-exhaustive pattern matching
  2. error propagation
  3. early returns
  4. error recovery
  5. default value(which in Roc is a form of error recovery).

So maybe we should sit down and catalog all of the different things we are trying to solve for, where they can occur, and potential syntactic constructions to solve for reach. Hopefully that way we can winnow down to a consistent set of syntax that solves these problems in a way that seems consistent and well thought out.

And selfishly in a way where I know what to implement in the parser :rofl:

view this post on Zulip Richard Feldman (Feb 28 2025 at 22:25):

here is a concrete proposal that I'm happy with. If anyone would rather we didn't do this, please say why!

  1. Change the way ?? return "" formats to have the formatter add braces so it becomes ?? { return "" } and same with crash. No changes to the semantics of anything involved in this; it's purely to address the concern over it being not visually obvious enough what the code does.
  2. Introduce if Ok(path) = paths.first() { pattern matching. We explored a bunch of alternatives and this still feels like the best solution to the problem at the top of the thread.

that's it, no other changes. Both of these are addressing specific ergonomics concerns with the status quo, and are not trying to go back to the drawing board and reconsider everything.

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:25):

Sounds like we want to address these use cases:

  1. I want to be able to return early from a function when I encounter a Result that is an Err
  2. I want to do the same as 1, but change or wrap the Err that will be returned
  3. I want to be able to crash or return early when a non-exhaustive pattern match fails in a terse form of syntax
  4. I want to be able to recover from a non-exhaustive pattern match failing
  5. I want to be able to return early when a Boolean condition fails

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 22:26):

Sam Mohr said:

The point of something like res ? error_mapper

But what about chaining?

Anthony Bullard said:

It feels like we want to overhaul the entire syntax in the language...

That's probably my bad :sweat_smile: it just everything pulls everything and as a result, I see a combinatorial explosion in variants

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:27):

Richard Feldman said:

here is a concrete proposal that I'm happy with. If anyone would rather we didn't do this, please say why!

  1. Change the way ?? return "" formats to have the formatter add braces so it becomes ?? { return "" } and same with crash. No changes to the semantics of anything involved in this; it's purely to address the concern over it being not visually obvious enough what the code does.
  2. Introduce if Ok(path) = paths.first() { pattern matching. We explored a bunch of alternatives and this still feels like the best solution to the problem at the top of the thread.

that's it, no other changes. Both of these are addressing specific ergonomics concerns with the status quo, and are not trying to go back to the drawing board and reconsider everything.

This is the value of having a BDFN

view this post on Zulip Sam Mohr (Feb 28 2025 at 22:28):

When you ask "what's with chaining", just wanna clarify that binop ? can only be used at the end of a chain, as is the case with ??. Only suffix ? can be used in chains

view this post on Zulip Sam Mohr (Feb 28 2025 at 22:29):

Just for formatting, do we ever force newlines for a single statement brace block?

view this post on Zulip Richard Feldman (Feb 28 2025 at 22:29):

nah

view this post on Zulip Sam Mohr (Feb 28 2025 at 22:29):

Yeah, sounds good

view this post on Zulip Sam Mohr (Feb 28 2025 at 22:29):

The alternative would suck for tersity

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:32):

Today all blocks (with braces) are multiline in the new formatter

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:32):

So that will be a sort of custom/new change

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:33):

The formatter actually doesn't use braces at all for single statements blocks that have no newlines or comments

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:34):

It should be easy to change now that I get rid of Body/Block as a separate type and they are just exprs now

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 22:34):

Anthony Bullard said:

Sounds like we want to address these use cases

I would also add aesthetics/ergonomics dimensions such as

view this post on Zulip Anthony Bullard (Feb 28 2025 at 22:36):

Short circuit works really well in too cases: Boolean operations and in languages with a concept of null. In a language like Roc where Result is just a builtin datatype (but not primordial to the language) our only real choice is recovery or early return

view this post on Zulip Kiryl Dziamura (Feb 28 2025 at 22:57):

Yeah, I mean short circuiting as conditional evaluation of expressions. If such expression is a block, it may end with a return or crash statement which makes early return possible. So early return makes sense only inside of "short circuits". That's how I see it.

view this post on Zulip Richard Feldman (Feb 28 2025 at 23:16):

semantically, I think it's simpler for return and crash to be expressions, but I think formatting them to have braces around them after a ?? looks better :big_smile:

view this post on Zulip Richard Feldman (Feb 28 2025 at 23:16):

I don't think we should introduce more complicated rules than "they're expressions" though

view this post on Zulip Richard Feldman (Feb 28 2025 at 23:17):

the downside of "if they're expressions, it means you can technically use them in a way that you'd never use them" seems inconsequential to me :stuck_out_tongue:

view this post on Zulip Richard Feldman (Feb 28 2025 at 23:17):

and everybody already knows all the rules around expressions, so it doesn't require introducing a new concept to the language

view this post on Zulip Richard Feldman (Feb 28 2025 at 23:17):

they just go in the expression bucket with everything else

view this post on Zulip Niclas Ahden (Feb 28 2025 at 23:21):

  1. Change the way ?? return "" formats to have the formatter add braces so it becomes ?? { return "" } and same with crash. No changes to the semantics of anything involved in this; it's purely to address the concern over it being not visually obvious enough what the code does.

What's the visually confusing part of ?? return "foo"? Isn't it as confusing/clear as foo() ? Bar? (kmn if I accidentally just triggered adding braces around that as well :joy:)

view this post on Zulip Niclas Ahden (Feb 28 2025 at 23:27):

?, ? , and ?? are some top-tier features and they existing specifically to make error handling ergonomic so that people will expend the energy to add tags/context (which they otherwise don't, as experience has shown us). Shouldn't we have a high bar for making these features less ergonomic?

view this post on Zulip Brendan Hansknecht (Mar 01 2025 at 00:05):

Now that we have static dispatch do we expect to see issues with ? and ?? ?
They no longer play nice with pipelines

out =
    (((load_config(in_file) ?? default_config)
        .stage1(abc) ? Stage1Err)
        .stage2() ? Stage2Err)
        .stage3() ? Stage3Err

view this post on Zulip Brendan Hansknecht (Mar 01 2025 at 00:06):

I think this is the root issue why we may want to nix or redesign them.

view this post on Zulip Richard Feldman (Mar 01 2025 at 00:15):

I think we should wait to see if it's a problem in practice

view this post on Zulip Richard Feldman (Mar 01 2025 at 00:16):

so far in all the cases I've seen in the realworld app, which uses dispatch everywhere, it wouldn't have mattered

view this post on Zulip Richard Feldman (Mar 01 2025 at 00:18):

and I think these operators have had a problem with scope creep, which makes me extra hesitant to redesign them for hypothetical problems that haven't come up in practice :big_smile:

view this post on Zulip Jacek Rembisz (Mar 03 2025 at 15:32):

Hello, I usually just read content here (it's like watching a good tv series to me :smile:) but this time I couldn't resist to add something to the story :upside_down: .

My proposal is to treat an if expression as a syntactic sugar for specific match expressions.
So an if expression in its full form:

if <cond > { patternIf => <exprIf> } else { patterElse => <exprElse> }

would be translated to:

match <cond> { patternIf => <exprIf> patternElse => <exprElse> }

but you can omit sam parts. If patternIf is omitted it is assumed to be True =>. If patternElse is omitted it is assumed to to be _ => . If whole else part is omitted it is assumed to be else { _ => {} }
Than you can write normal if expression:

if a < 5 { a } else { 5 }

but also use enhanced if expression when an equivalent match expression would be to verbose:

if paths.first() {Ok(path) => File.delete!(path)?}
if paths.first() {Ok(path) => File.read_utf8!(path)? } else { "" }
if my_list { [a] => calculate(a) } else { crash "expecting exactly one record" }
if some_condition {False => <look, this is important> } else { <never mind > }

From teaching perspective, you can introduce first a simple if expression then the match expression and than you can say: in case of such a simple, two-branch match expressions you can use this enhanced if expresion for short, and introduce the full form of if expression.

view this post on Zulip Anton (Mar 03 2025 at 15:38):

Welcome to the conversation @Jacek Rembisz :)

view this post on Zulip Derin Eryilmaz (Mar 03 2025 at 17:31):

Kiryl Dziamura said:

Can ?? be used only for the early return? The question marks suggests it in some sense. It might be implied as parametric unwrap where both the expression and the return value are not necessarily Results.

When you don't need early return - use .with_default

Ok(path) = paths.first() ?? "" # return empty string if not Ok

# desugars to
match paths.first() {
    Ok(path) => # continuation
    _ => ""
}

What about just special syntax to allow a single branch of a match to be a continuation?

match paths.first() {
    Ok(path) => ..
    _ => ""
}
# path is in scope here

...or for if statements:

# instead of
if (badCondition) { return Bad }
# other stuff

# what about
if (badCondition) { Bad } else ..
# other stuff

Last updated: Jun 16 2026 at 16:19 UTC