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:
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.
Putting the ? at the end of lines is a great way to fix our interrobang issue!
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)
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
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
So here's my whole dump of comments:
List.walk! would need to have an ! at the end of its name, just like top-level functions and record/tuple members?! 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.! 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...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?-> 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.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.
Sam Mohr said:
- 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?- 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.
Sam Mohr said:
Result.parallelmakes 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.parallelandThunk.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...
Sam Mohr said:
- 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 bewhen 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:
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 likefoo x y?means unwrapping theyargument 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:
await in front like in every other language?"something that I came to appreciate about ? at the end of the line is that that's where the control flow change happens
e.g. comparing these two:
foo? bar baz
foo bar baz?
what's happening in both cases is:
foo bar bazErr, short-circuitof the two options above, the second one is where ? appears right where the short-circuiting actually occurs
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
yeah separate thread makes sense :big_smile:
Kilian Vounckx said:
I just feel like a call like
foo x y?means unwrapping theyargument and not the entire call
of note, another possible design is to give the ? a space before it, such that:
foo bar baz ? means "call foo passing bar and baz and then short-circuitfoo bar baz? means "baz is a Result; short-circuit it and then pass its Ok to foo along with bar"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"?
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" ?
Seems reasonable
I can't wait for someone to get in a fight with the formatter over that
Haha
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.
Sam Mohr said:
- 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.
But the exact details there are an addon after the core proposal, so not paramount to figure out now
i think the version space before ? is much less ambiguous
I can read File.readUtf8! path ? as if there were parens File.readUtf8!(path)?
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)?
Yes. The initial release would be without effect polymorphism. We would re-evaluate if we actually want effect polymorphism later
So it would have walk and walk! as a pure and effectful version of each function
Now if you really want to make it mainstream, replace the ? with ; :big_smile:
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
?
Richard Feldman said:
Kilian Vounckx said:
foo bar baz ?means "callfoopassingbarandbazand then short-circuitfoo bar baz?means "bazis aResult; short-circuit it and then pass itsOktofooalong withbar"
This seems too easy to mess up. I don’t think white space should be meaningful outside of indentation.
I think most people would think those two lines are just styled differently
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 ...
So this is a function that returns a result containing a function? Or should this be invalid without parens?
foo bar ? baz
Definitely putting a ? after the args leads to a lot more edge cases than just leaving it at !?
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:
You'd probably get a type error anyway, though
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
What if (foo bar ?) returned a function?
Yeah, just using it as an example. Parens make it reasonable
Yep
I like the space before the ?, I also like that it ends the line so it feels like an extension of a |> Result.try ...
I feel like nesting ? in an expression could get confusing... but maybe it would be ok
If we start by requiring parens, the parenthesized code would still be valid if we later dropped the parens requirement
So it seems like an easy start at least
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
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
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
then we avoid any issues with operator precedence while still having a nice experience, and a more configurable one in userspace at that
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