Stream: ideas

Topic: subscript operator


view this post on Zulip Richard Feldman (Sep 13 2025 at 20:04):

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.

view this post on Zulip Richard Feldman (Sep 13 2025 at 20:05):

with static dispatch, it would be easy to support this: have foo[bar] desugar to foo.subscript(bar)

view this post on Zulip Richard Feldman (Sep 13 2025 at 20:07):

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

view this post on Zulip Richard Feldman (Sep 13 2025 at 20:09):

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

view this post on Zulip Richard Feldman (Sep 13 2025 at 20:10):

I think it would only exist for List, Dict, and Set among the builtins, because in strings it would be a footgun as usual.

view this post on Zulip Richard Feldman (Sep 13 2025 at 20:10):

given all that, I'm curious what others think of that idea!

view this post on Zulip Brendan Hansknecht (Sep 13 2025 at 20:48):

Return a result and being used with ? seems pretty reasonable

view this post on Zulip Fabian Schmalzried (Sep 13 2025 at 21:06):

I like it! List.subscript will basically be an alias for List.get but allows the []-operator to work?

view this post on Zulip Richard Feldman (Sep 13 2025 at 21:16):

yeah exactly

view this post on Zulip Brendan Hansknecht (Sep 13 2025 at 21:24):

Would it be supported for user defined types?

view this post on Zulip Fabian Schmalzried (Sep 13 2025 at 21:50):

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?

view this post on Zulip Richard Feldman (Sep 13 2025 at 21:55):

Brendan Hansknecht said:

Would it be supported for user defined types?

yeah same as any operator, it's just sugar for static dispatch

view this post on Zulip Richard Feldman (Sep 13 2025 at 21:56):

Fabian Schmalzried said:

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?

nothing enforced other than the name of the function and that it takes 2 arguments, just like the other operators

view this post on Zulip Brendan Hansknecht (Sep 13 2025 at 22:50):

Now you can make MyCrashingList with x[y] working like other languages....not a good idea, but why not I guess.

view this post on Zulip Isaac Van Doren (Sep 14 2025 at 02:53):

Love it!

view this post on Zulip Kiryl Dziamura (Sep 14 2025 at 16:00):

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

view this post on Zulip Kiryl Dziamura (Sep 14 2025 at 16:09):

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".

view this post on Zulip Richard Feldman (Sep 14 2025 at 16:24):

yeah I don't think we should go that far :smile:

view this post on Zulip Joshua Warner (Sep 18 2025 at 20:23):

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.

view this post on Zulip Richard Feldman (Sep 18 2025 at 21:11):

I have a separate design in mind for use cases like that, haven't written it up yet though!

view this post on Zulip Brendan Hansknecht (Sep 18 2025 at 21:31):

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.

view this post on Zulip Anton (Sep 19 2025 at 08:28):

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.

view this post on Zulip Richard Feldman (Sep 19 2025 at 13:48):

what do other languages do?

view this post on Zulip Martin Stewart (Sep 19 2025 at 14:32):

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?

view this post on Zulip Ian McLerran (Sep 19 2025 at 15:09):

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.

view this post on Zulip Ian McLerran (Sep 19 2025 at 15:13):

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...

view this post on Zulip Ian McLerran (Sep 19 2025 at 15:14):

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?

view this post on Zulip Kiryl Dziamura (Sep 19 2025 at 15:30):

Ian McLerran said:

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?

Because one can implement their own subscript for their types that may act not as getter per se

view this post on Zulip Brendan Hansknecht (Sep 19 2025 at 15:30):

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

view this post on Zulip Brendan Hansknecht (Sep 19 2025 at 15:31):

For example, for a Set, it maps to Set.contains

view this post on Zulip Norbert Hajagos (Mar 08 2026 at 17:41):

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) :

Personally considering the argument for consistency:

What do you think @Richard Feldman ?

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:01):

definitely rule out runtime panic

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:01):

the bar for that is way higher than this I think

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:02):

I think only integer overflow and OOM are the two places in the stdlib where crashing makes sense

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:02):

and div by zero on non-floats

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:03):

I'd say let's make them all Try for now and see how it goes

view this post on Zulip Richard Feldman (Mar 08 2026 at 19:04):

if it's super annoying in practice then we can use that data point to justify the other design

view this post on Zulip Norbert Hajagos (Mar 08 2026 at 19:34):

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.

view this post on Zulip Richard Feldman (Mar 08 2026 at 20:58):

awesome, thank you!

view this post on Zulip Kasper Møller Andersen (Mar 10 2026 at 05:18):

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:

view this post on Zulip Brendan Hansknecht (Mar 10 2026 at 05:34):

Hash and Eq are the requirements for a dict key

view this post on Zulip Brendan Hansknecht (Mar 10 2026 at 05:34):

Not that for most types, these should both just auto derive

view this post on Zulip Luke Boswell (Mar 10 2026 at 06:25):

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

view this post on Zulip Luke Boswell (Mar 10 2026 at 06:27):

So Dict will have where clause that constrain the key to a type that has both equals and hash implementations

view this post on Zulip Norbert Hajagos (Mar 10 2026 at 08:29):

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.

view this post on Zulip Lukas Juhrich (Mar 17 2026 at 19:49):

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 :)

view this post on Zulip Norbert Hajagos (Mar 17 2026 at 20:08):

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