Stream: ideas

Topic: Purity Inference proposal v2


view this post on Zulip Richard Feldman (Sep 04 2024 at 01:35):

starting a fresh thread (see #ideas > Purity Inference for the discussion of the first draft of the proposal) for a v2 of the proposal based on all the discussion last time!

any feedback welcome :smiley:

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 02:59):

I think starting without effect polymorphism is the right call. As we discussed before, we only expect a few higher-order functions to actually need it, and ! being part of the name makes it clear and straightforward if you need both versions.

view this post on Zulip Sam Mohr (Sep 04 2024 at 08:14):

Putting the ? at the end of lines is a great way to fix our interrobang issue!

view this post on Zulip Kilian Vounckx (Sep 04 2024 at 09:22):

Another precedent for a language using ! in function names is julia. There the bang is conventionally used to indicate the first function argument will be modified (e.g. transpose will return a new matrix, but transpose! transposes in place)

view this post on Zulip Kilian Vounckx (Sep 04 2024 at 09:25):

About the ? at the end of the call rather than after the function. In a language with roc (and ml/haskell) like syntax (i.e. no parens, but spaces) I feel like putting the ? after the function is clearer. Even at the cost of having !? I would prefer it. I don't know however how much of that is familiarity. I just feel like a call like foo x y? means unwrapping the y argument and not the entire call

view this post on Zulip Sam Mohr (Sep 04 2024 at 09:28):

I'm not sure how I feel about it, and I'm biasing in such a state of indecision towards the syntax that seems less confusing, and Rust precedence seems to imply that line-ending ? is less confusing

view this post on Zulip Sam Mohr (Sep 04 2024 at 09:44):

So here's my whole dump of comments:

  1. I looked through all the code examples, and I didn't see any effectful functions passed as parameters named. I presume the accumulator for List.walk! would need to have an ! at the end of its name, just like top-level functions and record/tuple members?
  2. Is the plan for us to throw only a warning if the user forgets/intentionally omits the ! suffix, and escalate that to an error later if we think it should be mandatory? If so, a warning makes sense, but if this proposal wants mandatory !, it should probably be an error.
  3. I think if we go with effect polymorphism, then having effect polymorphic functions need a name ending with ! instead of the ! being an operator is misleading for the pure case. If I pass a pure function to List.walk!, it's now pure, but the doc says "! means calling this function might do an effect." It also caveats that "any ! function being called within a pure function must be effect-polymorphic and not effectful (so, not =>).", but functions sans signatures will still be confusing. Though it's more complex, it seems easier to have the rule that "! means that an effect will happen, not may happen". Though this now conflicts with pure function bodies being annotated as effectful for encapsulation/API contract reasons, so I guess I don't know what to think...
  4. Result.parallel makes sense if results are returned, but what if I just want thunks that are infallible? It might be better to have a single module that platforms export with, say, Thunk.parallel and Thunk.parallel!. It can't be in the stdlib, right? Do we guarantee platforms can run things in parallel?
  5. Minor, but not a fan of snake_case for Roc, buuut not the place for this discussion.
  6. I think the visual distinction for "when-then" vs. just arrows has some merit, but not enough to lose the orthogonal syntax that -> gives. However, I think there's real potential in using the effectful => arrow in effectful contexts! If we don't have effect polymorphism, then effectful functions could be written \x => Stdout.line! x, and effectful branches could be when x is; Ok y => Stdout.line! y. I'd love to discuss this further if someone else likes the idea.

view this post on Zulip Sam Mohr (Sep 04 2024 at 09:46):

And I agree with Agus, if we can make a Roc without effect polymorphism work and the ecosystem doesn't look to want to bifurcate, that seems much better for learnability/conceptual simplicity.

view this post on Zulip Richard Feldman (Sep 04 2024 at 10:17):

Sam Mohr said:

  1. I looked through all the code examples, and I didn't see any effectful functions passed as parameters named. I presume the accumulator for List.walk! would need to have an ! at the end of its name, just like top-level functions and record/tuple members?
  2. Is the plan for us to throw only a warning if the user forgets/intentionally omits the ! suffix

if it's a function that runs effects, then yeah it would need a ! in its name!

Regarding warning vs. error, by design we don't have a CLI flag for ignoring warnings, and they cause a nonzero exit code, so they fail CI just the same as errors do until you've addressed them all. Also, both warnings and errors are designed to be nonblocking; compiler bugs aside, you should always be able to run the program no matter how many warnings or errors you have (although you may get a crash at runtime).

So the only difference between warnings and errors is how they're presented to the user, and I see the relevant distinction there as being whether it could lead to a runtime crash. For example, unused imports are not going to crash anything, so those are warnings. A non-exhaustive pattern match is an error because it could lead to a crash.

So this would be a warning.

view this post on Zulip Richard Feldman (Sep 04 2024 at 10:23):

Sam Mohr said:

  1. Result.parallel makes sense if results are returned, but what if I just want thunks that are infallible? It might be better to have a single module that platforms export with, say, Thunk.parallel and Thunk.parallel!. It can't be in the stdlib, right? Do we guarantee platforms can run things in parallel?

I'm not sure what the module name should be (I agree that Result would be strange if it can't fail), but I think it can be a builtin. The basic idea would be that it would compile down to exactly the same thing as Task.map2 today, so the host just ends up seeing "here are some things that I'm supposed to run in parallel" - but the host might be single-threaded, in which case it shrugs and runs them sequentially.

The point would be to express "I'd like these to run in parallel if possible because I think that will be faster," but if they can't be, then running them sequentially should never cause a bug - it just wouldn't be as fast as desired. But of course even if they're actually running in parallel, there's no guarantee of performance - for example, the program might be running in a virtualized environment where only one CPU core is available, or maybe there are other processes competing for cores and this one doesn't get as much parallelism as it wanted, etc...

view this post on Zulip Richard Feldman (Sep 04 2024 at 10:24):

Sam Mohr said:

  1. I think the visual distinction for "when-then" vs. just arrows has some merit, but not enough to lose the orthogonal syntax that -> gives. However, I think there's real potential in using the effectful => arrow in effectful contexts! If we don't have effect polymorphism, then effectful functions could be written \x => Stdout.line! x, and effectful branches could be when x is; Ok y => Stdout.line! y. I'd love to discuss this further if someone else likes the idea.

that's interesting, although if when does it then it might be weird for if not to do it :thinking:

view this post on Zulip Richard Feldman (Sep 04 2024 at 10:51):

Kilian Vounckx said:

About the ? at the end of the call rather than after the function. In a language with roc (and ml/haskell) like syntax (i.e. no parens, but spaces) I feel like putting the ? after the function is clearer. Even at the cost of having !? I would prefer it. I don't know however how much of that is familiarity. I just feel like a call like foo x y? means unwrapping the y argument and not the entire call

that's what I thought at first, but honestly I got used to it pretty quickly.

it reminded me of .await in Rust in that:

view this post on Zulip Richard Feldman (Sep 04 2024 at 11:37):

something that I came to appreciate about ? at the end of the line is that that's where the control flow change happens

view this post on Zulip Richard Feldman (Sep 04 2024 at 11:39):

e.g. comparing these two:

foo? bar baz
foo bar baz?

what's happening in both cases is:

  1. run foo bar baz
  2. then, if it returned an Err, short-circuit

of the two options above, the second one is where ? appears right where the short-circuiting actually occurs

view this post on Zulip Kilian Vounckx (Sep 04 2024 at 11:55):

This does make sense actually. So basically ? is just lower precedence than function calling. Over time I think I will get used to it as well.
Slight aside, would it make sense to give unary prefix operators the same precedence? Such that -foo x becomes -(foo x) instead of (-foo) x. Maybe this should be a new thread

view this post on Zulip Richard Feldman (Sep 04 2024 at 12:24):

yeah separate thread makes sense :big_smile:

view this post on Zulip Kilian Vounckx (Sep 04 2024 at 12:42):

done

view this post on Zulip Richard Feldman (Sep 04 2024 at 16:51):

Kilian Vounckx said:

I just feel like a call like foo x y? means unwrapping the y argument and not the entire call

of note, another possible design is to give the ? a space before it, such that:

view this post on Zulip Richard Feldman (Sep 04 2024 at 16:52):

here's the example from the proposal:

    # Check jq version
    Cmd.exec! "jq --version"?

    # Create the build directory
    if File.exists! "build"? then
        Dir.deleteAll! "build"?

    Dir.create! "build"?

    (path, data) = List.first inputs?
    populate! path data (Dir.list! "src"?)?

    # Download the latest examples
    Cmd.exec! "curl -fL -o examples-main.zip https://…"?
    Cmd.exec! "unzip -o examples-main.zip"?

view this post on Zulip Richard Feldman (Sep 04 2024 at 16:52):

here it is with ? having a space before it:

    # Check jq version
    Cmd.exec! "jq --version" ?

    # Create the build directory
    if File.exists! "build" ? then
        Dir.deleteAll! "build" ?

    Dir.create! "build" ?

    (path, data) = List.first inputs ?
    populate! path data (Dir.list! "src"?) ?

    # Download the latest examples
    Cmd.exec! "curl -fL -o examples-main.zip https://…" ?
    Cmd.exec! "unzip -o examples-main.zip" ?

view this post on Zulip Anton (Sep 04 2024 at 16:56):

Seems reasonable

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 17:57):

I can't wait for someone to get in a fight with the formatter over that

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 17:57):

Haha

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 17:59):

I'm +1 overall for this proposal. Not a fan of the "when then" aside, but I am good with all of the core proposal.

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 18:02):

Sam Mohr said:

  1. I think if we go with effect polymorphism, then having effect polymorphic functions need a name ending with ! instead of the ! being an operator is misleading for the pure case.

Yeah, if we add effect polymorphism, I think we should make effect polymorphic functions require ! based on use.

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 18:02):

But the exact details there are an addon after the core proposal, so not paramount to figure out now

view this post on Zulip Romain Lepert (Sep 04 2024 at 19:34):

i think the version space before ? is much less ambiguous

I can read File.readUtf8! path ? as if there were parens File.readUtf8!(path)?

view this post on Zulip drew (Sep 04 2024 at 19:48):

this write up is great. one thing i'm unsure of -- is the proposal suggesting excluding effect polymorphism for the initial release and having both walk and walk! (for example)?

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 20:10):

Yes. The initial release would be without effect polymorphism. We would re-evaluate if we actually want effect polymorphism later

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 20:11):

So it would have walk and walk! as a pure and effectful version of each function

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 20:49):

Now if you really want to make it mainstream, replace the ? with ; :big_smile:

view this post on Zulip Sam Mohr (Sep 04 2024 at 21:18):

For formatting the trailing ? for multiline function calls, I vote the ? stays on the same line as the last arg:

List.tryWalkBackwardsWithIndex
    allOfMyItems
    (Ok initialState)
    accumulator ?

it seems better than

List.tryWalkBackwardsWithIndex
    allOfMyItems
    (Ok initialState)
    accumulator
    ?

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 21:42):

Richard Feldman said:

Kilian Vounckx said:

This seems too easy to mess up. I don’t think white space should be meaningful outside of indentation.

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 21:45):

I think most people would think those two lines are just styled differently

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 21:54):

I wouldn't mind if the formatter added a space, but parens were still required to short-circuit baz, like:

foo bar baz ?       # foo bar baz |> Result.try ...
foo bar (baz ?)     # baz |> Result.try \bazOk -> foo bar bazOk
foo bar (baz ?) ?   # baz |> Result.try \bazOk -> foo bar bazOk |> Result.try ...

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 22:08):

So this is a function that returns a result containing a function? Or should this be invalid without parens?

foo bar ? baz

view this post on Zulip Brendan Hansknecht (Sep 04 2024 at 22:09):

Definitely putting a ? after the args leads to a lot more edge cases than just leaving it at !?

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 22:14):

I think either option is fine in that example. I would say maybe require parens because most times that'd likely be a mistake, like you accidentally hit J in vim or something :smile:

view this post on Zulip Agus Zubiaga (Sep 04 2024 at 22:15):

You'd probably get a type error anyway, though

view this post on Zulip Sam Mohr (Sep 05 2024 at 00:53):

Brendan Hansknecht said:

So this is a function that returns a result containing a function? Or should this be invalid without parens?

foo bar ? baz

I'd vote parse error/type error, the reader would need to know some specific and probably non-intuitive rules otherwise

view this post on Zulip Sam Mohr (Sep 05 2024 at 00:56):

What if (foo bar ?) returned a function?

view this post on Zulip Brendan Hansknecht (Sep 05 2024 at 01:26):

Yeah, just using it as an example. Parens make it reasonable

view this post on Zulip Sam Mohr (Sep 05 2024 at 01:27):

Yep

view this post on Zulip Luke Boswell (Sep 05 2024 at 01:29):

I like the space before the ?, I also like that it ends the line so it feels like an extension of a |> Result.try ...

view this post on Zulip Luke Boswell (Sep 05 2024 at 01:30):

I feel like nesting ? in an expression could get confusing... but maybe it would be ok

view this post on Zulip Sam Mohr (Sep 05 2024 at 01:30):

If we start by requiring parens, the parenthesized code would still be valid if we later dropped the parens requirement

view this post on Zulip Sam Mohr (Sep 05 2024 at 01:31):

So it seems like an easy start at least

view this post on Zulip Andrea Bueide (Sep 06 2024 at 20:35):

may be unpopular opinion but I kinda like tying desugaring operators to the expression they're desugaring so it feels kind of like an alias for the the Result.try or Task.await where you're passing in the expression as an argument kind of. Maybe its just my aversion to making ? look like a parameter wildcard or something, or that it operates on a parameter.

readAndWrite! : Str => Result Dec _
readAndWrite! = \path =>
    str = ?File.readUtf8! path
    pct = 100 * ?Str.toDec str
    ?File.writeUtf8! path "Percent: $(Num.toStr pct)"
    Ok str

view this post on Zulip Andrea Bueide (Sep 06 2024 at 20:47):

but also I'm kind of just leaning against using ! as a naming convention at all, it really does take up a prime spot for function call modifiers (like ?, probably incorrect terminology) without making it look weird. I think just having pure/impure be part of the type signature / type inference system is good enough

view this post on Zulip Andrea Bueide (Sep 06 2024 at 20:51):

it just feels like this would look so much nicer and be more configurable as an lsp/editor feature with type hints than as a naming convention

view this post on Zulip Andrea Bueide (Sep 06 2024 at 20:52):

then we avoid any issues with operator precedence while still having a nice experience, and a more configurable one in userspace at that

view this post on Zulip Richard Feldman (Sep 06 2024 at 21:14):

as it turns out, v3 of the proposal (which I just posted) addresses these concerns! :smiley:

https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Purity.20inference.20proposal.20v3


Last updated: Jun 16 2026 at 16:19 UTC