Simple proposal, change List.repeat from a, Nat -> List a to { elem: a, len: Nat } -> List a.
Of course, the exact record field names can change. The main reason I want this change is because I regularly mix up the parameter order when dealing with integer constants. For example, List.repeat 0 1000. You can't tell if that is a 0 filled list of length 1000 or a 1000 filled list with 0 elements. I think this is a case where being more explicit will help avoid bugs/annoying debugging.
Slight tangent - but Rust Analyzer adds helpful parameter name annotations to call sites (at least in VSCode). If the Roc editor did the same, does that solve this (without adding extra typing)?
I like the idea. I guess you lose the ability to use easily in a pipeline though. "-" |> List.repeat 10 etc
I feel like List.repeat is pretty rare in pipelines except at the beginning, but I could definitely be wrong for other people's use cases
Also, annotations would theoretically help, but I prefer to have it directly in the text file because who knows what editor i am using, if I have the lsp, and how it is configured. every parameter name at a roc call sight might look really odd and hard to read given the syntax.
Personally I’ve ran into this exact same confusion with parameter order for List.repeat or List.fill or whatever the language I’m using happens to call it multiple times so I’d love this change. Definitely outweighs being able to pipe IMO
An approach that could continue to support pipelines would be to keep 2 separate arguments, but require that they be wrapped in tags. E.g. [Elem a], [Len Nat] -> List a
Admittedly, it feels a bit strange to use tags in a context where there is only one allowed value.
Interesting then in a pipeline, you would do: "-" |> Elem |> List.repeat (Len 10)
Of course with "-" coming from some previous pipeline stage instead of by constant
Is there a name for this kind of pattern? Where you pass in a configuration record instead of invidiual parameters to a fn?
Maybe there is a natural rule for when this is a good idea?
If we look at some other examples; would these work too?
Str.split {value: Str, separator: Str} -> StrStr.startWith {value: Str, substring: Str} -> BoolStr.endsWith {value: Str, substring: Str} -> BoolStr.replaceEach {value: Str, target: Str, replace: Str} -> Result Str [Notfound]Str.replaceFirst {value: Str, target: Str, replace: Str} -> Result Str [Notfound]Str.replaceLast {value: Str, target: Str, replace: Str} -> Result Str [Notfound]Str.splitFirst {value: Str, delimiter: Str} -> Result {before: Str, after: Str} [Notfound]Str.splitLast {value: Str, delimiter: Str} -> Result {before: Str, after: Str} [Notfound]Actually.. This gave me an idea..
What about a, { len: Nat } -> List a?
You can then do List.repeat 0 {len: 1000} and "-" |> List.repeat {len: 100} etc. Resolves your original concern, and is a pattern that can be easily repeated.
Str.split Str, {separator: Str} -> StrStr.startWith Str, {substring: Str} -> BoolStr.endsWith Str, {substring: Str} -> BoolStr.replaceEach Str, {target: Str, replace: Str} -> Result Str [Notfound]Str.replaceFirst Str, {target: Str, replace: Str} -> Result Str [Notfound]Str.replaceLast Str, {target: Str, replace: Str} -> Result Str [Notfound]Str.splitFirst Str, {delimiter: Str} -> Result {before: Str, after: Str} [Notfound]Str.splitLast Str, {delimiter: Str} -> Result {before: Str, after: Str} [Notfound]I don't think this is an issue for most of those string function, at least not as bad.
With roc's focus on pipelining, it is expected that the first argument of a function will be what is expected to be pipelined. The core data being operated on.
As such, Str.split first arg will be the data that is being split.
etc
With List.repeat on the other hand, it doesn't really have core data that I would expect to be pipelined in
I could see someone picking either order for the arguments
calculateLen ... |> List.repeat "x" or calculateValue ... |> List.repeat 100
What do you think of a, { len: Nat } -> List a though?
With just one field in the record, it feels weird that it isn't a tag. I would probably write a, Len Nat -> List a, but otherwise, I definitely like the idea. That said, if a lot of functions change to an api like this, I think the record syntax may make more sense due to cases with multiple named args.
Yeah another one for example; List.intersperse : List elem, {sep : elem} -> List elem
Not sure why that one is elem instead of a in the docs... we seem to be switching randomly for some reason
Probably a different person wrote the function signature.
Why would you do it to List.intersperse? types are clear and there is nothing that could get mixed up?
haha, though the example in the doc is backwards. Though it may have been written before we had pipelining and updated all of the function arg orders.
I know this is silly, but I made a version with tags that supports either order:
repeat : [Elem a [Len Nat], Len Nat [Elem a]] -> List a
repeat = \param ->
when param is
Elem elem (Len len) | Len len (Elem elem) -> List.repeat elem len
"-" |> Elem (Len 10) |> repeat
Tags are awesome. :grinning:
are there any overheads caused by passing records instead of plain values?
nope
David Mell said:
An approach that could continue to support pipelines would be to keep 2 separate arguments, but require that they be wrapped in tags. E.g.
[Elem a], [Len Nat] -> List a
I think we should try this!
I don't know of any precedent for any other language trying it, so the only way we'll find out if it's nice or not is if we try it :big_smile:
and on paper it seems to have the properties we're looking for:
:check: clearly disambiguates which argument is which
:check: works in pipelines
we also have a bit of precedent for using tags to label arguments with List.range
(of course there it's multiple tags, so a more common use of tags, but it's something where you see tags being used at the call site)
dank said:
are there any overheads caused by passing records instead of plain values?
Technically, yes. In these cases there shouldn't be, but this isn't universally true. It is a complex picture that is hardware dependent and depends heavily on the exact types. For most small records it should be the same cost. For medium records, it depends, but i would lightly expect raw arguments to do better. For large records i would expect the record to could be faster than the raw args.
Note: in most cases i would not expect a roc user to ever think about this cost. The only time I would expect it to make a real difference is in passing around a large config to a ton of functions or recursively. That config could be put in a record to pass it around as one value instead of many. That could alleviate register pressure and make the program run faster.
Last updated: Jun 16 2026 at 16:19 UTC