Sometimes I want to iterate through a range like (range 100)
, but in reverse order. For this, there are two existing choices:
1) The programmer thinks about the would be the right start/end/step to construct the reversed range, and constructs the range accordingly, with a negative step:
(range 99 -1 -1)
2) Use reverse
as in (reverse (range 100))
Solution 1 is not great since well it is harder to think about because you have to change the start/end because of the reversed bounds inclusive/exclusive bounds on the start and end. It would also require more mental math than I like to do, in the case where step
is not 1. For instance, quickly solve how to reverse (range 23 96 7)
! The answer is (range 93 22 -7)
, but it's not ideal to have to do this math in your head and to commit it to code when really what you meant was (reverse (range 23 96 7))
. For instance, if you tomorrow want to change the lower bound from 23 inclusive to 24 inclusive, you have to actually recompute the upper bound and change the code to (range 94 23 -7)
.
This would get even more difficult/impossible for the programmer if they're using non-integer ranges:
(range 1.5 5.4 1.23)
Solution 2 is not great since it takes O(n) space and time:
(defn reverse
"Returns a seq of the items in coll in reverse order. Not lazy."
{:added "1.0"
:static true}
[coll]
(reduce1 conj () coll))
So then what?
Reversible
Maybe the clojure.lang.Range
/clojure.lang.LongRange
objects could implement clojure.lang.Reversible
, since they can be reversed in O(1) time and space, with something like:
public ISeq rseq() {
final Number difference = Numbers.minus(end, start);
final Number remainder = Numbers.remainder(difference, step);
final Number last = Numbers.isZero(remainder) ?
Numbers.minus(end, step) : Numbers.minus(end, remainder);
final Number reverseStep = Numbers.minus(step);
BoundsCheck bc;
Object newEnd;
if (Numbers.isPos(reverseStep)) {
newEnd = Numbers.inc(start);
bc = positiveStep(newEnd);
} else {
newEnd = Numbers.dec(start);
bc = negativeStep(newEnd);
}
return new Range(last, newEnd, reverseStep, bc);
}
And then the reversing could be done in clojure using the rseq
function
(rseq (range 10))
=> (9 8 7 6 5 4 3 2 1 0)
Problems with this approach and/or challenges:
Still doesn't work on "empty ranges" because when you do (range 0 0)
you actually get an empty list (not a Range object actually). So since lists aren't Reversible, you'd get an exception when you try to rseq it. This could be fixed by allowing there to be an empty Range object, or perhaps by making empty lists Reversible? Anyways probably too aggressive of a change. I actually don't think there should be a problem with allowing an empty Range, it's not clear to me if it's important that empty ranges are specifically lists, but for instance it isn't the case that the empty Vector is a list, or that the empty Set/Map are the empty list, so it doesn't seem to be bizarre that there would be an empty Range.
some more thought would have to go into the algorithm to compute the reverse range in the case of floating point ranges, since the above algorithm doesn't produce the exact same result in reverse. For instance:
(range 1.5 5.4 1.23)
=> (1.5 2.73 3.96 5.1899999999999995)
(rseq (range 1.5 5.4 1.23))
=> (5.1899999999999995 3.9599999999999995 2.7299999999999995 1.4999999999999996)
If not that, then maybe :reverse true
keyword arg
Maybe the reversibility belongs more in the range
function:
(range 10 :reverse true)
or similar.
Anyways I am not saying Clojure's wrong or anything, just submitting some food for thought. Thanks for reading!