this is the last of a batch of related proposals over the last few weeks - any feedback welcome! :smiley:
https://docs.google.com/document/d/1Ly5Cp_Z7dY8KLQkkDYZlGCldxQj4jLzZ0vIeB-F8lJI/edit?usp=sharing
Can I pass a var as a module param? Or is that top level within the module?
yeah I'd say we shouldn't allow that...could probably make it work, but would probably be confusing :big_smile:
That sounds like a wise choice :octopus:
Would the _ suffix for names of vars be enforced by a warning?
"enforced" might not be the right word there, I'm just wondering if there would be a warning
In general, I like how readable the code is here. I am somewhat worried that I do not have a clear mental model for what it means for a variable to be "reassignable". From the proposal I gather that it is different enough from actually being mutable to preserve the important pure functional guarantees, but I don't quite grok why.
IIUC the main difference is that the mutation is only local to the current function. You cannot pass a mutable reference and have that function mutate it, so there's no "spooky action at a distance".
Agus Zubiaga said:
Would the
_suffix for names ofvars be enforced by a warning?
yep, that's the idea!
Should there be a compiler warning for a var that is not reused?
I would say yes, and also for a name with an _ suffix that is not a var.
So having a var function argument would be like having a normal argument which you can alter in the function, but the caller can't see the changes?
Like are these equivalent?
foo = \var x_ ->
...
foo = \x ->
var x_ = x
...
But the type for this function would still be e.g. foo : Str -> Str, not foo : var Str -> Str, because an outside user can't detect that it changes?
right, there's no var in types
The proposal says this though?
In the uncommon situation that you want to annotate a var's type, you'd use the var keyword in the annotation too:
var name_ : Str
var name_ = "(name goes here)"
So this is only for variables declared locally then?
ah, so to clarify the distinction there:
var only ever appears right before a namevar never appears inside the type itselfit's like var vs const in Zig, or perhaps var (or let) vs const in JavaScript
it's really about "can I have this name refer to something different later on"
and says nothing at all about mutating values in place
At first I though the _ suffix was redundant and maybe giving more meaning to a name than it might be expected, but this changed my mind:
any time you see blah_ being passed to a function, you know to be aware that the value is changing
That's a really valuable guarantee that I don't think any language has.
take-my-money.gif
3 messages were moved from this topic to #ideas > Augmented assignment operators for var by Agus Zubiaga.
Could we omit the var keyword and instead require the trailing underscore to identify a def as shadowable?
You kind of need both, because var lets you know where the first definition is. You don't need it for normal definitions because they can only be defined once anyway
But you don't have that for "shadowable" variables. Not important for short function bodies, definitely for long ones
Is that necessary though? It seems like it would be about as easy to scan down a function looking for the first use of a particular name as it would be to find it when var is included.
Not necessary for sure. But I think readability is better, and communicates more clearly to users what we mean, especially for function args
I don’t know that it is necessarily more readable. I’m not a big fan of the fact that both var and trailing underscore are used to communicate the same thing and that they are completely coupled. It seems like two things to learn and explain when only one is necessary.
I agree that the most important thing is the underscore suffix, and without the var keyword this proposal is still good to go basically
I just don't think that duplication is always bad.
Though Roc at its core is built on concision between not needing to put let x = ... and the fully decidable type inference
Meaning the var prefix is unusual
Which added to its pairing with the underscore suffix makes it more unnecessary
Of note, you could use var in front of any pattern, not just = patterns - e.g. in function arguments, when branches, or destructures:
What happens if I use var when destructuring a record? Would I have to do something like this? {foo: foo_} if I want to shadow foo?
Also, would var be allowed when pattern matching on tags with no payload even though there would be nothing to shadow?
Isaac Van Doren said:
Of note, you could use var in front of any pattern, not just = patterns - e.g. in function arguments, when branches, or destructures:
What happens if I use var when destructuring a record? Would I have to do something like this? {foo: foo_} if I want to shadow foo?
I think it'd be:
{ foo: var foo_ }
Isaac Van Doren said:
Also, would var be allowed when pattern matching on tags with no payload even though there would be nothing to shadow?
I think var would just be a qualifier for Identifier patterns, not just any pattern. So putting it before a tag name would be syntax error.
Okay cool, glad it would be a syntax error there
Another benefit of requiring var is that it makes defining mutable variables more awkward than immutable ones, incentivizing pure code where possible
shared this in DMs but i would consider renaming var to mut - this isn't really shadowing per-se because you can't change the type of a var-bound name, and so it is better to think of it as mutation. Also, using var might overload what it means for something to be a "variable" (is it any bound name or a var-bound name?)
the reason var is important is for the following scenario:
var, and the underscore suffix means that it's reassignablefoo_ and reassigns it, as normal.foo_ is in scope, I start writing some new code. I happen to also use the name foo_ for this new code, not realizing that I am referring to a foo_ that already exists in the current scope.var fixes that because now if I'm introducing (what I believe is) a new usage of foo_ in the nested scope, I'll start by writing var foo_ and immediately get a shadowing error
Ayaz Hafiz said:
shared this in DMs but i would consider renaming
vartomut- this isn't really shadowing per-se because you can't change the type of a var-bound name, and so it is better to think of it as mutation. Also, usingvarmight overload what it means for something to be a "variable" (is it any bound name or a var-bound name?)
I think this would be confusing because mutation usually refers to changing values in-place. Like if I say fn = \arg1, mut list -> - then I expect to be able to mutate that list in-place, like I can in Rust, e.g. by calling push on it. But I wouldn't be able to! list is still an immutable value, even though I put mut in front of it.
Ayaz Hafiz said:
shared this in DMs but i would consider renaming
vartomut- this isn't really shadowing per-se because you can't change the type of a var-bound name, and so it is better to think of it as mutation. Also, usingvarmight overload what it means for something to be a "variable" (is it any bound name or a var-bound name?)
If you look at our "shadowing" in loops, it really doesn't look like shadowing because you're re-shadowing the same var in the same code location multiple times. That implies mutability to me
yeah that's fair. Maybe something like rebind or another word like that. i think you're not going to want to use this too often so a longer name that better communicates the intent may be better.
reass, nailed it :100:
I could see an argument for let
on the grounds that it follows the JS definition of let, but it's weird for an ML family language to have let mean "I'm opting into reassignment" :sweat_smile:
Ayaz Hafiz said:
using
varmight overload what it means for something to be a "variable" (is it any bound name or a var-bound name?)
:thinking: does this problem come up in JS or Zig?
like in both languages you have a cultural norm to default to const foo = ... but do people refer to foo in that situation as a "variable" or "constant"?
I'd think whatever they do could be what we do too
in the sense that either people say "variable" and mean both, and it's not confusing in practice, or else they say "constant" and "variable" and the distinction is clear
let would probably be more confusing since the ML family is let-bound immutable variables
yeah for sure
Could also use def or shadow
Alternative syntax idea:
each List.toIter paths is
path -> write! path "file contents"
What I like about this is it's similarity to the when .. is expression, that the expression iterated over appears before the pattern which feels more natural to me, and that there's more space for the 'iteration expression', which the examples in the proposal demonstrate can get pretty long.
Downside is that it looks less like a for loop so I imagine there's a higher learning cost.
I think var is a super cool idea, this combination of it being reassignable but not across function boundaries, seems super useful for loops and shadowing while at the same time hard to abuse in ways that would lead to readability problems. The one thing I'm wondering is whether it will feel artificially limiting to use, particularly to new folks coming from non-functional languages, resulting in frustrating rather than delightful experiences. One way to find out!
Is there a feasible way to sugar-away the List.toIter in for?
# Before
for path in List.toIter paths do
write! path "file contents"
# After (this reads nicer, removes one thing a beginner has to learn, and probably covers the majority of loops)
for path in paths do
write! path "file contents"
# Before
for (index, path) in List.toIter paths |> Iter.enumerate |> Iter.rev do
try write! path "stuff"
# After
for (index, path) in paths |> Iter.enumerate |> Iter.rev do
try write! path "stuff"
Whenever I have to type into_iter and collect in Rust I go "Come on compiler, just figure it out. What else do you think I meant?" This is probably me being a spoiled brat, but I can't help but reminisce about the good old Ruby days when iteration was clean and simple. I know I'll forever trip up on this and go "oh, right, and List.toIter" :clown:
So the current joins API in roc-pg's query builder looks like this:
products <- from Public.products
orderProducts <- join Public.orderProducts (on .productId products.id)
...
The reason backpassing is used is that a State monad is used internally to track things like used table aliases (in case you join the same one twice).
Of course, I'm going to have to change the API once we remove it. We could just use regular closures, but that'd result in very nested queries when there are many joins.
The solution is probably going to be to explicitly thread the state. However, that can be annoying without shadowing. Would var allow me to do something like this?
(products, var s_) = from Public.products
(orderProducts, s_) = join Public.orderProducts (on .productId products.id) s_
I don't know if the second bind to s_ would be allowed
Of course, this is not specific to roc-pg, we'd also want that for e.g. random generator seeds
yeah that's the idea!
destructuring record and tuple patterns before = work the same way as using = and dot accessors
I think for cases like this and like random seeds we could consider allowing standalone var s_ with no = as long as all subsequent code paths assign it before it gets read
Rust supports that with standalone let
Nice!
Richard Feldman said:
I think for cases like this and like random seeds we could consider allowing standalone
var s_with no=as long as all subsequent code paths assign it before it gets read
I don't know if standalone var makes much of a difference for this use case, though
That said, I can see it being useful for when you set it under if / when
I recently saw this tweet and when I saw the loop proposal in the google doc, I was immediately reminded of it: https://x.com/ChShersh/status/1837413739265413248
I have to say that I was a bit shocked to see this (I guess I've long since swallowed the "functional kool-aid"), but perhaps there is a point, if this is continually a sticking-point for newcomers to functional languages or to Roc. And if you can keep the ease of use of an imperative-looking way while preserving functional semantics underneath, then why not? Sounds like all-win, no-drawbacks to me (and those ARE occasionally possible!)
yeah one way to interpret that image is:
or maybe to say it another way, the things on the left are helper functions for common uses of the thing on the right
also no longer prone to subtle mistakes
Yeah, I definitely remember making lots of mistakes with for loop indices
I'm definitely a fan of this proposal in general
Really like the clear suffix that will be seen everywhere
Also think it is nicer than shadowing in general
Explicit instead of implicit
One situation where I would prefer shadowing is for example when assigning the result of List.walk. If you do the following sum = List.walk list 0 \sum, x -> sum + x, it does not compile because sum is shadowed, even though the result isn't used until after the List.walk is done. I think this has to do with making nested functions automatically recursive, but in this case it has been annoying to me.
Kilian Vounckx sagde:
One situation where I would prefer shadowing is for example when assigning the result of
List.walk. If you do the followingsum = List.walk list 0 \sum, x -> sum + x, it does not compile becausesumis shadowed, even though the result isn't used until after theList.walkis done. I think this has to do with making nested functions automatically recursive, but in this case it has been annoying to me.
Isn't the real problem that the shadowing analysis sees the two sums as the same, when the left-hand sum should only really exist in the analysis after the right-hand side has been executed? Logically, I think the analysis should detect these two sums as completely distinct.
I think this is there because the left-hand sum could be a function. In this case, it needs to be in scope to be able to call itself recursively. Not sure if this is the reason though. And it could be fixed by only doing this for functions I think
you can still have recursion by way of a higher-order function, e.g.
foo = fn \arg -> foo (arg + 1)
...where fn returns a function
I'm having a tough time picturing what living with underscore would look like vs actual shadowing. Below I have the same code in both Roc and F# and I would like to know how the Roc code would change if this feature is added.
Edge : { src : U64, dst : U64 }
parseInput : Str -> List Edge
parseInput = \string ->
List.walk (Text.lines string) ([], Dict.empty {}) \(edges, map), line ->
(vertex, nodes) = Text.bisectOn line ": " |> \(x, y) -> (x, Text.words y)
(src, aMap) = Dictx.computeIfAbsent map vertex \_ -> Dict.len map
List.walk nodes (edges, aMap) \(rEdges, rMap), node ->
(dst, sMap) = Dictx.computeIfAbsent rMap node \_ -> Dict.len rMap
(List.append rEdges { src, dst }, sMap)
|> fst
type Edge = { src : int; dst : int }
let parseInput str : list<Edge> =
Seq.fold (fun (edges, map) line ->
let (vertex, nodes) = String.split ": " line |> (fun [|x; y|] -> (x, String.words y))
let (src, map) = Map.computeIfAbsent (fun _ -> Map.count map) vertex map
Seq.fold (fun (edges, map) node ->
let (dst, map) = Map.computeIfAbsent (fun _ -> Map.count map) node map
edges @ [{ src = src; dst = dst }], map
) (edges, map) nodes
) ([], Map.empty<string, int>) (String.lines str)
|> fst
Last updated: Jun 16 2026 at 16:19 UTC