almost all mainstream languages have syntax like list[index] - which is known as the "subscript operator."
I remember someone mentioning that not having this was a major ergonomics problem when comparing Roc to Python for their use case, because they did a ton of positional list access for math, and the extra verbosity really affected readability.
with static dispatch, it would be easy to support this: have foo[bar] desugar to foo.subscript(bar)
I also vividly remember being in the room at Strange Loop 2015 when a roomful of Elm beginners was trying to do mob programming to learn the language, and the problem they chose was Game of Life. I wasn't supposed to say anything bc I knew Elm, but it was extremely painful watching them try to write it with subscript operators (that didn't exist in the language) and then fall down over and over
I think it would be reasonable to support this as long as we didn't follow Rust's design choice to crash on overflow - e.g. there'd be a lot of list[index]? in practice because List.subscript would return a Result just like List.get
I think it would only exist for List, Dict, and Set among the builtins, because in strings it would be a footgun as usual.
given all that, I'm curious what others think of that idea!
Return a result and being used with ? seems pretty reasonable
I like it! List.subscript will basically be an alias for List.get but allows the []-operator to work?
yeah exactly
Would it be supported for user defined types?
Then this would also allow using tags or something else? my_grid[Left] or chess_board[A1]? Would it be enforced that subscript returns a a Result?
Brendan Hansknecht said:
Would it be supported for user defined types?
yeah same as any operator, it's just sugar for static dispatch
Fabian Schmalzried said:
Then this would also allow using tags or something else?
my_grid[Left]orchess_board[A1]? Would it be enforced thatsubscriptreturns a aResult?
nothing enforced other than the name of the function and that it takes 2 arguments, just like the other operators
Now you can make MyCrashingList with x[y] working like other languages....not a good idea, but why not I guess.
Love it!
Would it work in the opposite way? As a setter? For variable_s? E.g.
x_ = List.create()
x_[0] = List.create()
x_[0]?[0] = 42
It's ok if not. It's not clear what to do if the setter returns Result after all.
I just find it's neat that when you see trailing underscore - you know exactly what you're "mutating".
yeah I don't think we should go that far :smile:
We can certainly _cross that bridge later_ - but IMO that's an obvious missing piece if both (1) locally-mutable variables, and (2) subscripting on lists, both exist. I predict that will be an early ask by anyone who uses that combo of features.
I have a separate design in mind for use cases like that, haven't written it up yet though!
Separately, but related.... List.set and List.replace silently doing nothing on a wrong index feels like a mistake. Given how much roc focuses on correctness, I feel like this probably should defailult to crashing as an out of bounds access. Same as an integer overflow. They feel related to me.
Given how we are returning Result for other List functions, Result would have my preference. Perhaps we can also provide a crashing set_unsafe and replace_unsafe if that runs faster.
what do other languages do?
Richard Feldman said:
given all that, I'm curious what others think of that idea!
What if instead of supporting subscripts the compiler showed an error explaining what to use instead? Additionally the formatter could replace the subscript with the correct function call.
That seems like it would solve the beginner confusion you saw with Elm?
An error message might solve some beginner confusion, but not so much the ergonomics issue Richard was describing. I think this design seems reasonable. I dont see any down sides. Nice quality of life when doing a lot of list access, with the added benefit of an easier onboarding curve for new developers.
I guess the one downside could be some confusion for new devs, when someone finds that var = list[sub] "just works", but list[sub] = var does not. An error message like @Martin Stewart was describing might be helpful in this case...
My question is why desugar to List.subscript, if subscript is just an alias for get? Is there a reason not to just desugar to List.get?
Ian McLerran said:
My question is why desugar to
List.subscript, ifsubscriptis just an alias forget? Is there a reason not to just desugar toList.get?
Because one can implement their own subscript for their types that may act not as getter per se
It will desugare to module.subscript. Given get can do different things and is a kinda common name, it is nice to decouple it from this operator
For example, for a Set, it maps to Set.contains
Brendan Hansknecht said:
Separately, but related.... List.set and List.replace silently doing nothing on a wrong index feels like a mistake. Given how much roc focuses on correctness, I feel like this probably should defailult to crashing as an out of bounds access. Same as an integer overflow. They feel related to me.
I have a PR coming up that will make List.subscript an alias to List.get.
I was about to start implementing List.replace and List.set, but hit this exact question.
Richard Feldman said:
what do other languages do?
Me and gemini have searched for a while. Proof based langs like Lean can say "first, give me a proof that the index is not out of bounds".
It's hard to find a language that doesn't just blow up on the out of bounds scenario. They are (pretty much) universally treated as "contract violation". There are the langs that throw exceptions like Java, C#. While technically recoverable, catching these is "widely considered" (according to gemini) a code smell. You should be checking the length before, not catching the error, so I've grouped these together.
I've found these that fit the "return a result" behavior, but not more:
These act like Roc (rust-based compiler) :
index is out of bounds, the original list is returned." Personally considering the argument for consistency:
list[too-big-number], not when setting it. In roc, accessing an element returns a Try. Thus, I'd rule out the runtime panic option and choose between the silent unmodified return and returning a Try. List.update is a no-brainer. I don't think Dict.remove, List.dropAt, Set.remove should return a Try, but one could make the argument in the name of consistency.What do you think @Richard Feldman ?
definitely rule out runtime panic
the bar for that is way higher than this I think
I think only integer overflow and OOM are the two places in the stdlib where crashing makes sense
and div by zero on non-floats
I'd say let's make them all Try for now and see how it goes
if it's super annoying in practice then we can use that data point to justify the other design
Just like how we tried default checked math operations. Sounds good! Weekend just ended, but I'll make the implementation according to that, when I get there.
awesome, thank you!
What’s the mechanism in Roc that allows people to type X as a Dict key? Must the type be hashable for instance? Or can’t people use arbitrary types for keys, but only strings and numbers?
Because I’m wondering if this would be a roundabout way of providing that capability. As in, if you want to use your own type as a Dict key, create a wrapper type around Dict with a custom insert and subscript function. You still want to be able to use this CustomDict with all the normal Dict functions of course, so this still leaves a gap, but just for brainstorming a bit :slight_smile:
Hash and Eq are the requirements for a dict key
Not that for most types, these should both just auto derive
I think the plan is to integrate this with static dispatch.
## Path.roc
Path := [
Unix(List(U8)),
Windows(List(U8)),
FromStr(List(U8)),
].{
from_bytes : List(U8) -> Path
from_bytes : |bytes| …
to_bytes : Path -> List(U8)
to_bytes = |path| …
equals : Path, Path -> Bool
hash : _
}
See the two standalone type annotations at the end:
equals : Path, Path -> Bool
hash : _
You define the type and the compiler infers the implementation if it's one of the supported ones, like equals -- or you can provide your own implementation
So Dict will have where clause that constrain the key to a type that has both equals and hash implementations
Yes, and the subscript method for Dict will be just an alias to its Dict.get method, which already states that its first argument is hashable.
sorry to necrobump, but before I forget:
when thinking about indexing we should also think about whether and if yes how to support string slicing.
I've been dearly missing that in roc, but letting string[i] effectively be string.to_utf8()[i] would be quite unfortunate because it returns the ith byte and not the ith character. Determining the ith character is inefficient with variable-character-width backed layout however because basically the whole string needs to be traversed.
IIRC python works around that by choosing a byte size according to the maximum byte size of a contained unicode character and then we know to look at offset character_width * i.
Sorry if any of this is half-informed, feel free to correct me :)
No worries necro all you want :)
iirc indexing into a string with a get method is prohibited in roc, so string[i] won't be allowed either. I think the same goes for slicing; same footgun. We'll have a dedicated package for working with unicode, which will surely have some way to get the subset of the string. For example, have a custom nominal type ListOfRunes or similar which can have a substring method.
It's a different story whether or not to have a slicing syntax, which would have a dedicated method as well. Then it could have a similar approach where ListOfRunes.slice would be an alias to in ListOfRunes.substring and slice would enable the syntax string[start:end] or something similar .I'd suggest making a separate thread inside the ideas channel, if you think this is something that would benefit roc.
Last updated: Jun 16 2026 at 16:19 UTC