Stream: ideas

Topic: Tuple rest patterns


view this post on Zulip Joshua Warner (Feb 06 2023 at 16:27):

Interesting

view this post on Zulip Joshua Warner (Feb 06 2023 at 16:30):

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)

view this post on Zulip Joshua Warner (Feb 06 2023 at 16:31):

That would also allow handling records in a generic fashion by "peeling" off one known field at a time

view this post on Zulip Joshua Warner (Feb 06 2023 at 16:33):

In this context, I'm imagining fmt would be an ability method of some kind

view this post on Zulip Joshua Warner (Feb 06 2023 at 16:35):

That would require being able to express the type of "a tuple of [possibly] distinct element types, all of which implement the Fmt ability"

view this post on Zulip Joshua Warner (Feb 06 2023 at 16:35):

Anyway - just thinking about future extensions. (a, ..) seems reasonable for now.

view this post on Zulip Brian Carroll (Feb 06 2023 at 17:14):

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?

view this post on Zulip Ayaz Hafiz (Feb 06 2023 at 17:15):

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

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:17):

Correct. The hypothetical future extension I described above would involve making tuples of length 0 and 1 parsable + valid.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:18):

(which they're not currently)

view this post on Zulip Folkert de Vries (Feb 06 2023 at 17:19):

hmm never been a fan of 1-element tuples

view this post on Zulip Folkert de Vries (Feb 06 2023 at 17:19):

they are just weird in python, rust and haskell

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:20):

Yeah I'd generally agree

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:21):

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

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:21):

They're pretty rare to actually need

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:24):

I don't want to get too caught up in the tangent here

view this post on Zulip Brian Carroll (Feb 06 2023 at 17:25):

Good explanation, thanks.

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 17:32):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:38):

Remember you're going to have to construct that large tuple somewhere in the first place.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:38):

There's no way to build up a large tuple out of small tuples.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:40):

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?

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:41):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:42):

... and tuples would also allow items of different types, unlike arrays/lists

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:47):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:48):

In that context, the user is (probably?) expecting all that code to be monomorphized and inlined anyway, which would eliminate the refcount overhead.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:48):

We could emit a warning about long tuple sizes

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 17:48):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:49):

Just a normal struct by that definition? Structs don't have an extra indirection for each element.

Yes

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:49):

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

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 17:49):

How would it?

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:50):

expecting all that code to be monomorphized and inlined anyway

Like this

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 17:50):

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

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:50):

They're all going to end up as locals at the llvm layer

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:50):

And llvm is going to work its magic

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:51):

(due to inlining)

view this post on Zulip Ayaz Hafiz (Feb 06 2023 at 17:55):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 17:55):

Maybe someone with admin perms can move everything after my initial "Taking a little bit of a tangent" post to a new topic?

view this post on Zulip Notification Bot (Feb 06 2023 at 17:56):

37 messages were moved here from #ideas > Should tuples be extendable in patterns by Ayaz Hafiz.

view this post on Zulip Ayaz Hafiz (Feb 06 2023 at 18:02):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 18:17):

(non-type-theorist here...) Can you expand on what rank-2 types are, and what makes this a rank-2 type?

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 18:17):

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.

view this post on Zulip Joshua Warner (Feb 06 2023 at 18:34):

Awesome idea on mimicking this in rust. Turns out LLVM is not a magical everything-optimizer. Consider me surprised :)

view this post on Zulip Joshua Warner (Feb 06 2023 at 18:37):

That means if/when we do this, they'll be some extra work pre-llvm to make it fast.

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 19:02):

haha. Yeah. llvm can do a lot, but memory and data movement causes a lot of pain points.

view this post on Zulip Brendan Hansknecht (Feb 06 2023 at 19:04):

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