Stream: show and tell

Topic: nice use of `??` and `return`


view this post on Zulip Richard Feldman (Dec 28 2024 at 23:44):

a nice use of the ?? operator with early return:

method_and_path = request.method_and_path() ??
    return Response.err(400).body("Bad HTTP method: ${method}")

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:45):

this works because ?? doesn't desugar to with_default but rather to this:

method_and_path =
    when request.method_and_path() is
        Ok(method_and_path) -> method_and_path
        Err(_) -> return Response.err(400).body("…")

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:54):

Is that a feature or a bug?

Like isn't that just the binop version ?? Just reimplemented via ???

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:56):

Oh, this is subtly different. Binop for ? wraps and error and this must return an error.

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:56):

yeah, binop ? is if you want to do something with the Err

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:56):

which this one doesn't

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:56):

This is returning a success. It is just a success that returns a 400 http code

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:56):

also, binop ? takes a lambda, so early return wouldn't work with it

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:56):

right, this whole function has the type Request => Response

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:56):

Yep

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:57):

so it's nice to be able to turn a Result into a Response like this! :smiley:

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:57):

especially right up front without indenting

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:57):

(the classic "early return" benefit)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 23:57):

It's a useful variant of Result.onErr that is already unwrapped

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:57):

well Result.on_err couldn't work here

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:58):

because it takes a lambda

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:58):

and return inside the lambda returns from the lambda, not from the outer function (which is what we want here)

view this post on Zulip Anthony Bullard (Dec 28 2024 at 23:58):

Yeah, but it might solve _some_ of the same usecases, but with less noise

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:58):

oh sure, it's great to have both

view this post on Zulip Anthony Bullard (Dec 28 2024 at 23:58):

And plus the early return which is nice

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:58):

like this wouldn't work in the middle of a pipeline of calls

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:58):

but for early return, it's definitely nice!

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:58):

Yeah, if you made errors explicit, it would be something like:

method_and_path = request.method_and_path() ?
    |_| { err_code: 400, body: "..." }

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:59):

yeah if the function returned Result Response ... then that would work

view this post on Zulip Richard Feldman (Dec 28 2024 at 23:59):

but in this case it's desirable to have it return Response instead of Result

view this post on Zulip Brendan Hansknecht (Dec 28 2024 at 23:59):

Yeah

view this post on Zulip Anthony Bullard (Dec 29 2024 at 00:00):

Cool here's another good usecase for ?? @Anton :smile:

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 00:14):

This looks a bit like Zig's catch keyword, which I'm quite fond of. catch allows capturing the error value though.

If I came across this pattern while reviewing code I think I would add a note to it, because I think it's almost always a mistake to drop an error value. When the error value is not incorporated into the return value somehow I'd expect it to get logged, so there's some trail to follow when debugging. For instance, in the 400 response example above, to help figure out why a server is returning an unexpected 400 error.

view this post on Zulip Richard Feldman (Dec 29 2024 at 00:18):

in this case that method only returns one error

view this post on Zulip Richard Feldman (Dec 29 2024 at 00:18):

so there isn't any relevant info to drop here

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 09:05):

Fair point, but:

It comes down to the sugar containing an Err(_) branch and so using it means giving up on some exhaustiveness checking with all the associated risks and benefits.

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 09:11):

For calling effectful functions, I believe we have the rule that if you're not interested in the return value, you have to do:

_ = runEffect!()

And I believe the plan is that the _ = can be ommitted if the return value is {}.

Is this the same situation where we want to help against accidentally dropping information? Or do we feel differently because it's an error value?

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 09:14):

What if the 'signature' of the ?? operator (and possibly Result.withDefault as well) becomes:

(??) : Result val err, (err => val) => val

method_and_path = request.method_and_path() ?? |_|
    return Response.err(400).body("Bad HTTP method: ${method}")

And make it so the |_| can be ommitted if the error type is {}?

view this post on Zulip Richard Feldman (Dec 29 2024 at 12:22):

hm, allowing the lambda to be omitted based on type information would mean this is no longer syntax sugar, and feels too complicated to me :sweat_smile:

view this post on Zulip Richard Feldman (Dec 29 2024 at 12:22):

and if it couldn't be omitted, then the early return wouldn't work here

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 13:38):

Richard Feldman said:

and if it couldn't be omitted, then the early return wouldn't work here

Is this because the return from return from the lambda instead of the containing function?

Maybe ?? |x| ... would constitute one sugar, so the whole thing is a 'fake lambda' with slightly different rules, but that sounds really confusing :sweat_smile:. Maybe there's a different syntax that does the same thing but without implying lambda-ness.

Personally, without the error capture I'd consider ?? a bit of a footgun, and would rather not have it, but I appreciate others might feel different :).

view this post on Zulip Anthony Bullard (Dec 29 2024 at 13:42):

Yeah, remember in Zig, || is a capture group, not a lambda. It's still a part of the containing function

view this post on Zulip Brendan Hansknecht (Dec 29 2024 at 15:01):

Jasper Woudenberg said:

It comes down to the sugar containing an Err(_) branch and so using it means giving up on some exhaustiveness checking with all the associated risks and benefits.

This confuses me a bit. The whole point of something like Result.withDefault is to remove the error case. If you are moving to the ok case only, I feel like it would be an antipattern to still depend on the error.

Dict.get dict key ?? default_value

view this post on Zulip Brendan Hansknecht (Dec 29 2024 at 15:03):

Anthony Bullard said:

Yeah, remember in Zig, || is a capture group, not a lambda. It's still a part of the containing function

I feel like that would lead to a lot of confusion and inconsistency in roc. In zig, it is a capture group cause they have no lambdas. They are a low level language with only function pointers.

view this post on Zulip Anthony Bullard (Dec 29 2024 at 15:07):

That was exactly my point, different languages with different idioms and constructs

view this post on Zulip Brendan Hansknecht (Dec 29 2024 at 15:08):

Ah, I thought you were suggesting we could do the same thing as zig. Definitely a misunderstanding of intent.

view this post on Zulip Anthony Bullard (Dec 29 2024 at 15:09):

Zig also has syntax in ifs for unwrapping optionals

view this post on Zulip Anthony Bullard (Dec 29 2024 at 15:09):

We should do us

view this post on Zulip Anthony Bullard (Dec 29 2024 at 15:09):

If you want to do something with the error, we have onErr, mapErr, and mapBoth

view this post on Zulip Brendan Hansknecht (Dec 29 2024 at 15:16):

Jasper Woudenberg said:

When the error value is not incorporated into the return value somehow I'd expect it to get logged, so there's some trail to follow when debugging.

Ah, I see your reasoning. I guess I rarely work on code where this is the case. Generally the with default is a semi-expected case and the error is not something to care about. Something akin to Dict.get with a default value being the most common use case.

Even in most zig code where I use catch it is generally catch unreachable or similar.

I can see how for high level logic like deciding an http 400 you might want logging or ab explicit not of no extra info to log. For me that is the exceptional case not the common case, so I didn't think of it.

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 20:25):

I didn't realize before that this "problem" (depending on how you feel about it) is inherent to Result.withDefault as well.

Brendan Hansknecht said:

Even in most zig code where I use catch it is generally catch unreachable or similar.

I do that too sometimes, but I think that's a different case. In case of a catch unreachable we don't take the error branch. I think the Zig equivalent of ?? would be catch return.

view this post on Zulip Brendan Hansknecht (Dec 29 2024 at 20:28):

Yeah, as I said as my first comment:

Is that a feature or a bug?

?? just forcing a default value makes sense to me. Using it with return feels much more accidental. Maybe it is good. Maybe it leads to ignoring error classes.

view this post on Zulip Jasper Woudenberg (Dec 29 2024 at 20:30):

Brendan Hansknecht said:

Something akin to Dict.get with a default value being the most common use case.

Yeah, that use doesn't seem particularly bad to me either, the Dict.get error is essentially equivalent to {}, so we're not losing any information.


Last updated: Jul 06 2025 at 12:14 UTC