Interesting
Taking a little bit of a tangent - what if we did this for both records and tuples (i.e. require .. if you want to match an unknown number of extra fields/elements) - and then (eventually, at some point in the future), make it possible to actually _name_ that and capture the extra fields in a variable. e.g.:
fmtTuple = \items ->
when items is
() -> ""
(a, ..rest) -> Str.concat (fmt a) (fmtTuple rest)
That would also allow handling records in a generic fashion by "peeling" off one known field at a time
In this context, I'm imagining fmt would be an ability method of some kind
That would require being able to express the type of "a tuple of [possibly] distinct element types, all of which implement the Fmt ability"
Anyway - just thinking about future extensions. (a, ..) seems reasonable for now.
Just noticing the () here. I want to check that I'm following this feature. Tuples are a separate thing from records in the type system, so this is a new zero-sized type, yes? And {} == () is a type error?
Pretty sure () does not parse, so it cannot actually end up in the type system. Or at least that is the intention - there are no empty tuples
Correct. The hypothetical future extension I described above would involve making tuples of length 0 and 1 parsable + valid.
(which they're not currently)
hmm never been a fan of 1-element tuples
they are just weird in python, rust and haskell
Yeah I'd generally agree
An alternative I've seen is to make them valid in the type system, but not parsable - e.g. force them to be constructed as Tuple.single 123
They're pretty rare to actually need
I don't want to get too caught up in the tangent here
Good explanation, thanks.
Joshua Warner said:
Taking a little bit of a tangent - what if we did this for both records and tuples (i.e. require
..if you want to match an unknown number of extra fields/elements) - and then (eventually, at some point in the future), make it possible to actually _name_ that and capture the extra fields in a variable. e.g.:fmtTuple = \items -> when items is () -> "" (a, ..rest) -> Str.concat (fmt a) (fmtTuple rest)
So trully making tuples into arrays, but where you can't index and instead have to do more complex pattern matching (just feels like traditional cons list pattern matching)?
My biggest concern here is that rest could have a very large cost and there is nothing to signal that to the user. I think we should not promote something like this. I think instead it should be explicit. If there are only a few items in a tuple, there is really no cost to writing things out explicitly. If the tuple is really large and basically an array, you really don't want this feature anyway, so writing it out with huge verbosity should make it clear to end users that it not a great idea.
Remember you're going to have to construct that large tuple somewhere in the first place.
There's no way to build up a large tuple out of small tuples.
My biggest concern here is that rest could have a very large cost
I assume this is because we'd have to copy <n> items to produce a new tuple that has a ref-counted header?
So trully making tuples into arrays, but where you can't index and instead have to do more complex pattern matching (just feels like traditional cons list pattern matching)?
This is not quite true - I'm imagining being able to have a tuple of inline structs or something - i.e. something where you don't have to have extra pointer indirection for each element.
... and tuples would also allow items of different types, unlike arrays/lists
I'm imagining this would mostly be useful in contexts where you'd want to make a printf-like function that takes a variadic number of arguments.
In that context, the user is (probably?) expecting all that code to be monomorphized and inlined anyway, which would eliminate the refcount overhead.
We could emit a warning about long tuple sizes
I assume this is because we'd have to copy <n> items to produce a new tuple that has a ref-counted header?
No refcounted header. Just stored on the stack unless boxed, but yes, coping <n> items to a new location on the stack
i.e. something where you don't have to have extra pointer indirection for each element.
Just a normal struct by that definition? Structs don't have an extra indirection for each element.
Just a normal struct by that definition? Structs don't have an extra indirection for each element.
Yes
Just stored on the stack unless boxed, but yes, coping <n> items to a new location on the stack
I feel like in practice this is going to be optimized away
How would it?
expecting all that code to be monomorphized and inlined anyway
Like this
The items will be stored in a different memory location with a different order based on the size of elements. It isn't even guaranteed to be in the same byte order and just be a memcpy
They're all going to end up as locals at the llvm layer
And llvm is going to work its magic
(due to inlining)
Can we spin this discussion off elsewhere? It's valuable and I don't want it to distract from the original intention of the thread.
Maybe someone with admin perms can move everything after my initial "Taking a little bit of a tangent" post to a new topic?
37 messages were moved here from #ideas > Should tuples be extendable in patterns by Ayaz Hafiz.
Joshua Warner said:
Taking a little bit of a tangent - what if we did this for both records and tuples (i.e. require
..if you want to match an unknown number of extra fields/elements) - and then (eventually, at some point in the future), make it possible to actually _name_ that and capture the extra fields in a variable. e.g.:fmtTuple = \items -> when items is () -> "" (a, ..rest) -> Str.concat (fmt a) (fmtTuple rest)
want to call out that in order for this to work, Roc would need to support some (smaller) form of rank-2 or dependent types and polymorphic recursion - which, for various reasons, the language does not presently intend to do. So that is another important consideration, besides how values get represented at runtime.
(non-type-theorist here...) Can you expand on what rank-2 types are, and what makes this a rank-2 type?
LLVM won't fix this.
Here is an example in rust that tries to model what roc would generate: https://rust.godbolt.org/z/cbnvo1EYe
If you look at the diff of the generated assembly: https://www.diffchecker.com/VIUzSPwV/
You can see the extra data movement operations in the version with the rest tuple existing.
Awesome idea on mimicking this in rust. Turns out LLVM is not a magical everything-optimizer. Consider me surprised :)
That means if/when we do this, they'll be some extra work pre-llvm to make it fast.
haha. Yeah. llvm can do a lot, but memory and data movement causes a lot of pain points.
We definitely could fix it before llvm to some extent. In this case, we would either need to manually inline and remove the intermediate type, or we would need to update the called function to essentially be like our partial record functions where they are can accept a larger record type but only use a subset of record fields. Both have a number of complexities and tradeoffs, but it is doable.
Last updated: Jun 16 2026 at 16:19 UTC