Stream: beginners

Topic: ✔ Postfix ? operator question


view this post on Zulip Jonathan (Jan 16 2025 at 01:39):

I've just been reading through the messages in the past few days - have been largely on the outside of Roc, seeing the occasional talk and blog post over the past couple years. Recently I realised I was really quite interested in seeing the development of this language, especially a functional one with such lofty and unique goals. So this is my part "hello!" message, but also because I'm really not sure if I've quite understood the following, I'm asking here in beginners.

It's my understanding that recently accepted semantics of foo() ? bar is to - in the case of an Err - map bar on the contained error value before returning early. What I don't understand is how this will affect the below examples:

Kilian Vounckx said:

Richard Feldman said:

yeah I think you'd need parens

In a longer pipe that might become more verbose

With parens:

(
    foo1()
    .foo2)
    .foo3() ? MapErr
)
.bar()
.baz()

Without parens:

foo1()
.foo2()
.foo3() ? MapErr
.bar()
.baz()

With map_err and postfix ?:

foo1()
.foo2()
.foo3()
.map_err(MapErr)?
.bar()
.baz()

Is the current consensus that parens will be needed to continue the chain? I.e., for the second option, it's my understanding that SD/methods would lead to (if bar was a valid function that could be applied to the error value) .bar().baz() being applied to the error value before returning. Or is this a case handled by whitespace delimiting the ? (which would be very surprising to me)? Furthermore, I'm really quite fond of being able to use multiple ? in a chain of method calls, so as to stay on the happy path - would that be facilitated?

I've been (unsuccessfully) trying to catch up and figure out where current opinions and changes lie, so I apologise if I've completely missed the mark here, but I am curious!

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 01:51):

Welcome! And that is a really good parsing question. Often for these kinds of syntaxes, you have to imagine what it would look like on a single line to see if the parsing makes sense.

I would assume that this would fail to parse:

foo1()
.foo2()
.foo3() ? MapErr
.bar()
.baz()

Imagine it on one line:

foo1().foo2().foo3() ? MapErr.bar().baz()

Without some form of parens, that really does not make sense.

That said, making it parse (which would require change precedence or whitespace rules) could be really powerful. It would be nice to facilitate something like:

x
.stage1() ? Stage1Err
.stage2() ? Stage2Err
.stage3() ? Stage3Err

But I definitely would not expect that to work by default. It would instead be a follow up syntax that we might enable if there is significant demand and it makes sense for the language.

That is at least my understanding, but someone else who know more about parsing may have corrections.

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 01:52):

So in a pipeline, you would have to write it out as:

x
.stage1()
.map_err(Stage1Err)?
.stage2()
.map_err(Stage2Err)?
.stage3()
.map_err(Stage3Err)?

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 01:52):

Or I guess add a ton of parens, but that would look really bad

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 01:56):

I've been (unsuccessfully) trying to catch up and figure out where current opinions and changes lie, so I apologise if I've completely missed the mark here, but I am curious!

I generally read ~100% of the messaeges that appear on the roc zulip and lately it has been going to fast for me to keep up. As such I have even skimmed or skipped large sections of discussion. So this is super undestandable

view this post on Zulip Luke Boswell (Jan 16 2025 at 02:01):

@Jonathan checkout #show and tell > roc-realworld initial exploration @ 💬

If you would like to see the "future" syntax of Roc.

view this post on Zulip Jonathan (Jan 16 2025 at 02:01):

I see! So the simplified mapping syntax requires the whitespace, and cannot (as it stands) be used within a chain. For that, you would revert to the unsugared map_err version, with a ? and no whitespace? I'm just a little fixated on whitespace because it would mean that

x
.stage1()?
.stage2()

(where stage1 returns a Result that we are leaving as-is - no mapping) is semantically different from

x
.stage1() ?
.stage2()

In the latter, could it not be that stage2 is being mapped on any Err returned by stage1, whilst the former example early-returns the Err from stage1 unaltered, and otherwise unwraps the Ok and passes the contents to stage2?
I know this is quite a construed example (if it is even right), and would rely on stage2 being compatible with both Err and Ok values, but I still find it pretty high in terms of surprise or weirdness.

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:07):

Yeah, I'm not sure how we'll handle that. I would hope that we would just automatically reformat that to:

x
.stage1()?
.stage2()

But I guess it is possible that you really want:

x
.stage1() ? .stage2()

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:08):

being compatible with both Err and Ok values

Yeah, like if both your Ok and Err value were simply Str.

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:09):

I guess this is an argument for requiring |x| x.stage2() instead of allow .stage2() alone to mean that.

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:09):

Though I would assume this is a pretty rare mistake in practice (I mean, maybe a common typo, but very rare that it dosen't fail typechecking)

view this post on Zulip Sam Mohr (Jan 16 2025 at 02:14):

I think a lot of the proposed builder behavior strongly relies on having stuff like .stage2(), so I'd be really surprised if that did get implemented

view this post on Zulip Jonathan (Jan 16 2025 at 02:18):

(Wouldn't you also have the same issue with shorthand record access functions? (.field))

I suppose I'm coming at this and seeing PNC proposed (not mainly to be sure, but definitely in no small part) due to its larger scale familiarity, and how Gleam benefited from that, and I'm just wondering if in this case it's worth having two very similar looking ways to do this (.map_err(f)? and ? f) that could quite conceivably confuse someone, no less beginners (for instance, me). I suppose the type checker in this case could provide something like

did you mean .stage1()?
                    ^^^ ~~~ (No whitespace between ? and the ...

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:39):

Yeah, I think the common case won't be long pipelines, but many individual variables that each want to wrap their error.

x = do_foo!(a, b) ? FooErr
y = x.something() ? SomeErr
z = y.map(...) ?? ""

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:40):

Or if you don't care about error wrapping, just ? alone

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:41):

I do agree that it could just be left to .map_err and that wouldn't be too verbose:

x = do_foo!(a, b).map_err(FooErr)?
y = x.something().map_err(SomeErr)?
z = y.map(...).with_default("")

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:42):

I do like that the extra spacing makes it easier to separate the ok from the error path though. Kinda like the after thought was wrapping the error.

view this post on Zulip Brendan Hansknecht (Jan 16 2025 at 02:42):

Anyway, I'm sure we'll see how it pans out in practice and there will be multiple iterations on the error messages.

view this post on Zulip Luke Boswell (Jan 16 2025 at 02:58):

I think the idea is to encourage wrapping errors with Tags, to give additional context

view this post on Zulip Luke Boswell (Jan 16 2025 at 02:58):

So this makes it really easy to do that

view this post on Zulip Notification Bot (Jan 22 2025 at 16:53):

Jonathan has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC