Skip to content

Conversation

@usaoc
Copy link
Collaborator

@usaoc usaoc commented Jan 6, 2026

This PR addresses the lack of range-like sequences in descending order, as previously discussed on Discord. In summary:

  • <.. and <..= ranges with a starting point are now {Sequence,List}Range;
  • “Descending ranges” (that are not ranges) are added, and ListRange.descending produces (listable) descending ranges.

Methods that produce sequences are as follows:

(rge :: SequenceRange).to_sequence()             // `rge` by itself is specially inlined
(rge :: SequenceRange).step_by(step :: PosInt)   // specially inlined

(rge :: DescendingRange).to_sequence()           // `rge` by itself is specially inlined
(rge :: DescendingRange).step_by(step :: NegInt) // specially inlined

Descending ranges are of the following forms, each corresponding to a sequence range form:

start >=.. end  // .from_to
start >=..= end // .from_to_inclusive
start >=..      // .from
start >.. end   // .from_exclusive_to
start >..= end  // .from_exclusive_to_inclusive
start >..       // .from_exclusive

Descending ranges satisfy DescendingRange, which is not a subclass of Range. They only serve as expressions, not as bindings. Among them, listable ones also satisfy DescendingListRange, which offers a .to_list implementation.

Moreover, (rge :: ListRange).descending() also produces a descending range (more precisely DescendingListRange), and optimization in implicit .to_sequence and dot .step_by cooperate with it. This means that (0..5).descending().step_by(-2) as a sequence is specially inlined.

@usaoc usaoc force-pushed the descending-range branch from a09c119 to 4419da9 Compare January 6, 2026 21:00
@samth
Copy link
Member

samth commented Jan 6, 2026

This is precisely the operator names that @dgcosta2 used here https://github.com/dgcosta2/matrix/blob/main/operators.rhm so that seems like a good sign.

@mflatt
Copy link
Member

mflatt commented Jan 6, 2026

This looks great!

In your summary above, you annotated SequenceRange.step_by as specially inlined, and the improved documentation says the same thing. But aren't SequenceRange.step_by and SequenceRange.to_sequence both specially inlined (including prior to this commit)? Or am I missing something about they way they interact?

Having all ofSequenceRange.step_by, SequenceRange.to_sequence, and SequenceRange.ascending to do the same thing seems like a lot, but I guess the reason for so many aliases is connected to this part that I'm missing about for optimization.

@mflatt
Copy link
Member

mflatt commented Jan 6, 2026

I see that SequenceRange.step_by and SequenceRange.to_sequence do currently behave differently. I do not know (or have forgotten) why they need to, though.

@usaoc
Copy link
Collaborator Author

usaoc commented Jan 7, 2026

Explicit invocations of .to_sequence methods have never been specially treated, since Sequenceable objects (lists, arrays, maps, sets, ranges, etc.) by themselves are already specialized. This aligns with the behavior of a class with both implements Sequenceable and a sequence clause. For SequenceRange.step_by, I only implemented optimization of rge.step_by(step) (in this specific form), because this seems to be the most common form you would write. There isn’t any deep reason why more forms aren’t optimized.

Having both SequenceRange.to_sequence, SequenceRange.ascending, and SequenceRange.step_by does seem a bit bloated to me. I only implemented SequenceRange.ascending because it was suggested on Discord, but probably it isn’t needed since SequenceRange.step_by already does its job. Maybe SequenceRange.to_sequence should be kept 0-ary as well.

I wasn’t very sure about the design of ListRange.descending either. I thought about making it return a DescendingRange instead, and we then wouldn’t need a ~step argument since you can just use DescendingRange.step_by on it. This would also make a hypothetical DescendingListRange slightly more useful, because then you could write something like [& (0..5).descending()] (or maybe it’s still not very useful?). Moreover, this would justify actually specializing rge.descending() calls, which I hesitated about when it would produce a sequence.

@mflatt
Copy link
Member

mflatt commented Jan 7, 2026

Explicit invocations of .to_sequence methods have never been specially treated [...] aligns with the behavior of a class with both implements Sequenceable and a sequence clause.

Ah, right — I think that's how I had understood it previously, and forgot. We should write something like this in the documentation .to_sequence. Along those lines, I agree about keeping SequenceRange.to_sequence 0-ary.

I also like the ideas of having ListRange.descending produce a DescendingRange, relying on step_by for stepping, removing SequenceRange.ascending, and (maybe longer term) DescendingListRange.

usaoc added 3 commits January 10, 2026 05:02
Previously, only `..` and `..=` ranges were sequences/listables, on
the grounds that such a range has a definite first element, namely the
included starting point.  (And, more importantly, because the
precedents, Rust and Racket, didn't include `<..`- or `<..=`-like
ranges.)  I worried that there might be confusion regarding the first
element of a `<..` or `<..=` range, but it seems that making the first
included integer the first element is fine.  Moreover, this means that
a `<..` or `<..=` range would produce the same sequence/list as the
canonicalized range.
A "descending range" isn't actually a range, but is merely created for
the purpose of producing range-like sequences in descending order.
Since their purpose is for producing sequences, there are six forms in
total, each of which corresponds to one of the sequence range forms.
They are:

- `start >=.. end`, which corresponds to `start .. end`
- `start >=..= end`, which corresponds to `start ..= end`
- `start >=..`, which corresponds to `start ..`
- `start >.. end`, which corresponds to `start <.. end`
- `start >..= end`, which corresponds to `start <..= end`
- `start >.. end`, which corresponds to `start <.. end`

Each of the names starts with `>`, and I tried to keep with the
convention that `=` at the end indicates inclusiveness.  The forms
only serve as expressions, not as bindings, since they shouldn't work
as general range objects.  They all produce objects that satisfy
`DescendingRange`, which isn't a subclass of `Range`.

A `DescendingRange` can be used directly or with `.step_by` in `for`,
and will be optimized like its `SequenceRange` counterpart.  A
`ListRange` can be turned into a `DescendingRange` with `.descending`,
and the optimization cooperates with it, too.  So, for example,
`(0..5).descending().step_by(-2)` will be inlined.

Note also that `DescendingRange.step_by` takes an negative integer
step, unlike `SequenceRange.step_by`.
These are the possible results of `ListRange.descending`, which means
that `[& (0..5).descending()]` now works as expected.
@usaoc usaoc force-pushed the descending-range branch from 4419da9 to f444458 Compare January 9, 2026 21:38
@usaoc
Copy link
Collaborator Author

usaoc commented Jan 9, 2026

Updated to incorporate the suggestions, including DescendingListRange.

@mflatt
Copy link
Member

mflatt commented Jan 9, 2026

This is really great work — thanks! LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants