Stream: ideas

Topic: List.range boundaries


view this post on Zulip jan kili (Jul 06 2022 at 08:22):

What should these evaluate to?

List.range 1 4
List.range 2 3
List.range 2 2
List.range 3 2
List.range 4 1

I think everyone's opinions are welcome here, as this might be a design choice that's highly influenced by personal preferences and prior exposure.

view this post on Zulip jan kili (Jul 06 2022 at 08:31):

A) [2, 3]       ... []     ... []  ... []     ... []
B) [2, 3]       ... []     ... []  ... []     ... [3, 2]
C) [1, 2, 3]    ... [2]    ... []  ... []     ... []
D) [1, 2, 3]    ... [2]    ... []  ... [2]    ... [3, 2, 1]
E) [1, 2, 3]    ... [2]    ... []  ... [3]    ... [4, 3, 2]
F) [1, 2, 3]    ... [2]    ... [2] ... []     ... []
G) [1, 2, 3]    ... [2]    ... [2] ... [2]    ... [3, 2, 1]
H) [1, 2, 3]    ... [2]    ... [2] ... [3]    ... [4, 3, 2]
I) [1, 2, 3, 4] ... [2, 3] ... [2] ... []     ... []
J) [1, 2, 3, 4] ... [2, 3] ... [2] ... [3, 2] ... [4, 3, 2, 1]
K) Something else

view this post on Zulip Brian Carroll (Jul 06 2022 at 09:53):

I'm not sure I'm following the "syntax" of the examples!

view this post on Zulip Qqwy / Marten (Jul 06 2022 at 12:49):

Personally, I'd prefer the default for ranges to be half-open. This is a hotly debated topic of course, but Dijkstra had the following to say about 'indexes should start at zero' and 'ranges should be half open': https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html

view this post on Zulip Ayaz Hafiz (Jul 06 2022 at 12:55):

if half open means on the side such that List.range 1 4 = [1, 2, 3], I agree. this is the usual definition and seems quite common

view this post on Zulip jan kili (Jul 06 2022 at 13:25):

@Brian Carroll option E means

List.range 1 4 == [1, 2, 3]
List.range 2 3 == [2]
List.range 2 2 == []
List.range 3 2 == [3]
List.range 4 1 == [4, 3, 2]

view this post on Zulip Brendan Hansknecht (Jul 06 2022 at 13:53):

@JanCVanB you made a copy pasted error. Your first ³ lines all say 1 4 rn.

view this post on Zulip jan kili (Jul 06 2022 at 13:59):

Thank you! Fixed.

view this post on Zulip jan kili (Jul 06 2022 at 18:05):

Nice, it looks like there's consensus on preferring options C D E F G H. Any thoughts on reversibility, reversed open side, and empty ranges? Option E = reversible + right-side-open + emptyable.

view this post on Zulip jan kili (Jul 06 2022 at 18:06):

(deleted)

view this post on Zulip jan kili (Jul 06 2022 at 18:09):

I see option E as very natural, but I wanted to survey y'all in a somewhat unbiased way. (Especially since List.range is currently implemented as non-reversible and non-emptyable.)

view this post on Zulip Brendan Hansknecht (Jul 06 2022 at 20:52):

Yeah, I would pick E personally.

view this post on Zulip Brendan Hansknecht (Jul 06 2022 at 20:53):

Though I guess it does have the minor disadvantage that List.range 4 1 is not the reverse of List.range 1 4

view this post on Zulip Richard Feldman (Jul 07 2022 at 01:57):

what about labeled arguments?

List.range : { first : Int a, last : Int a } -> List (Int a)

this seems the least error-prone to me. There are conventions, but none of them are self-documenting; you always have to look it up, and if you're just glancing at someone else's code you might not spot a problem with it if you have the wrong mental model of what it's doing.

view this post on Zulip Richard Feldman (Jul 07 2022 at 01:58):

the first and last fields also tell you what the first and last elements of the list will be, so it should also work as expected no matter which is bigger than the other

view this post on Zulip jan kili (Jul 07 2022 at 02:10):

#LateralThinking

view this post on Zulip jan kili (Jul 07 2022 at 02:13):

That deviates from the "half-open" norm, but its explicitness avoids confusion

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 02:44):

I feel like generally as a programmer you want 0 to n-1 which is the only reason first and last feel weird

view this post on Zulip Brendan Hansknecht (Jul 07 2022 at 02:45):

Cause then you always are calculating n - 1, instead of just using n. But that is super minor

view this post on Zulip jan kili (Jul 07 2022 at 04:34):

I think the record nudges devs away from indexing hell, which I'd like: https://roc.zulipchat.com/#narrow/stream/231635-compiler-development/topic/indexing.20hell/near/288758539

view this post on Zulip Brian Carroll (Jul 07 2022 at 06:56):

So if I want to generate a range of length 10 starting from 20, then with the current API, I do this

start = 20
length = 10
List.range start (start+length)

But with the suggested API above, I do this

start = 20
length = 10
List.range { first : start, last : start + length - 1 }

view this post on Zulip Brian Carroll (Jul 07 2022 at 06:58):

This is quite prone to off-by-one errors.
So in my view, the reason the half-open design is more common is because very often in a real program you are calculating those parameters from a start and a length. Maybe in a loop (or recursion).
And then the half-open design is much easier to deal with.
I suspect that the "closed" version looks better and more intuitive in one-line examples in documentation but is more bug-prone in practice.

view this post on Zulip Ed Kelly (Jul 07 2022 at 07:06):

List.range {startAt: start, endBefore: start + length}?

view this post on Zulip jan kili (Jul 07 2022 at 11:49):

Polymorphism to the rescue?
List.range {start: 20, stop: 30}
List.range {start: 20, length: 10}
List.range {start: 20, finish: 29}

view this post on Zulip Anton (Jul 11 2022 at 13:28):

Perhaps we should have rangeIncl/rangeInclusive and rangeExcl/rangeExclusive and no range?

view this post on Zulip jan kili (Jul 11 2022 at 20:06):

I like that on first glance, but then would we need rangeInclusiveOnTheLeftSideButExclusiveOnTheRightSide?

view this post on Zulip Brendan Hansknecht (Jul 11 2022 at 20:32):

Inclusive range, exclusive range, and programmer range....yikes :grimacing:

view this post on Zulip Notification Bot (Jul 19 2022 at 15:14):

This topic was moved here from #compiler development > List.range boundaries by Anton.

view this post on Zulip jan kili (Jul 19 2022 at 15:15):

Martin Stewart said:

I've implemented nonempty versions of all the exposed List functions https://github.com/MartinSStewart/Nonempty.

One exception is List.range which currently returns [] if you call it with something like this: List.range 3 1. I was wondering if it would make more sense to have it instead return [ 3, 2, 1 ]? The reason is that then NonemptyList.range could be guaranteed to always return a nonempty list.

view this post on Zulip jan kili (Jul 19 2022 at 15:16):

@Martin Stewart please read the discussion above and let us know what you think!

view this post on Zulip Martin Stewart (Jul 19 2022 at 15:16):

I unfortunately can't see the earlier discussion. The first message I see is the Nofication Bot saying the topic was moved.

Nevermind, I just don't know how to use Zulip

view this post on Zulip jan kili (Jul 19 2022 at 15:17):

The best next step is probably to get one or two competing PRs going for the 2 most popular designs, so that people can see implementations and tests.

view this post on Zulip jan kili (Jul 19 2022 at 15:17):

@Anton HALP nvm

view this post on Zulip Martin Stewart (Jul 19 2022 at 15:30):

JanCVanB said:

Martin Stewart please read the discussion above and let us know what you think!

Those darn tradeoffs coming up all the time :sweat_smile: . I don't think I have anything to add besides the data point about how it makes my NonemptyList.range function nicer to use if it can have the exact same behavior as List.range. Which is only possible if List.range always returns a List containing at least one item (i.e. List.range 1 1 == [ 1 ] and List.range 3 1 == [ 3, 2, 1 ]).

view this post on Zulip jan kili (Jul 19 2022 at 15:34):

That makes sense! Thanks for adding that.

view this post on Zulip Qqwy / Marten (Jul 19 2022 at 15:36):

Unfortunately nothing prevents people from passing in ranges which are outside of the srange of the list, say List.range [1,2,3] 20 100.

view this post on Zulip Qqwy / Marten (Jul 19 2022 at 15:36):

So even if higher-to-lower ranges return elements, you still have to return a list and cannot return a NonEmpty

view this post on Zulip jan kili (Jul 19 2022 at 15:37):

Hmm, the current design(s) of List.range do not include a List parameter, what do you mean by that?

view this post on Zulip Qqwy / Marten (Jul 19 2022 at 15:38):

Sorry! I got confused with List.sublist :sweat_smile:

view this post on Zulip Qqwy / Marten (Jul 19 2022 at 15:38):

Disregard what I said

view this post on Zulip jan kili (Jul 20 2022 at 00:59):

:siren::loudspeaker: SURVEY TIME! All opinions welcome.
(though I'm unsure about using "@ all", haha)

How should List.range work?
A) List.range 1 3 == [1, 2]
B) List.range 1 3 == [1, 2, 3]
C) List.range { start: 1, stop: 3 } == [1, 2]
D) List.range { first: 1, last: 3 } == [1, 2, 3]
E) Something similar to __, but ____________.
F) Something completely different, specifically ____________.
G) There should be multiple builtin range functions.
H) There should be one builtin range function that accepts multiple input types. (Is this polymorphism? Idk.)

view this post on Zulip jan kili (Jul 20 2022 at 01:06):

Implications to note, for all Ints x and y:
A) List.range x x == [] and List.range x y != List.reverse List.range y x.
B) List.range x y != [] and List.range x y == List.reverse List.range y x.
C) see A
D) see B

view this post on Zulip jan kili (Jul 20 2022 at 01:08):

Also, a step/increment parameter can be easily added later, regardless of the basic design.

view this post on Zulip jan kili (Jul 20 2022 at 01:11):

</SURVEY-TIME></please-scroll-up-and-read-then-scroll-down-and-vote> :grinning_face_with_smiling_eyes::siren:

view this post on Zulip Brendan Hansknecht (Jul 20 2022 at 01:39):

My current leaning is G
Basically something like C and D. One with endAt (inclusive range on both sides) and the other with endBefore (inclusive start, exclusive end).

view this post on Zulip Thomas Dwyer (Jul 20 2022 at 02:54):

G as well. The name "range" through me off a little. I figured range would return the smallest and largest number in the list, but you meant enumeration. Perhaps something like the enumeration functions provided by Haskell's vector library would be better?

view this post on Zulip jan kili (Jul 20 2022 at 02:56):

Perhaps, for clarity, Num.range... would be a better namespacing than List.range... for the function(s) we're discussing.

view this post on Zulip Thomas Dwyer (Jul 20 2022 at 02:57):

although I prefer B/D's behavior, as it should end at rather than before the second argument.

view this post on Zulip Brendan Hansknecht (Jul 20 2022 at 04:27):

I think "ends before" is mostly a programming convenience due to often having a start and length. So then you have { start: start, endsBefore: start + length }.

Also, interesting point on naming, I guess range comes more from imperative language naming, but enumeration might before more common in functional languages? I guess you are thinking of the mathematical range of a function when you see List.range. always really interesting how different background bring very different naming expectations. Like when I see enumeration, I think of a c/c++ style enum or even rust. Which is more like a tag union in Roc.

view this post on Zulip Qqwy / Marten (Jul 20 2022 at 06:26):

I think it would be nice to have a dedicated Range datastructure as part of the standard library, with multiple constructors and maybe some light syntactic sugar, and then e.g. a List.fromRange.

view this post on Zulip Qqwy / Marten (Jul 20 2022 at 06:31):

Rust and Elixir are two examples of languages that thought that only supporting a single kind of range was fine, until a few years down the road it turned out that there was real benefit to supporting both half-open and inclusive ranges.

view this post on Zulip Anton (Jul 20 2022 at 07:54):

A dedicated Range datastructure does look like it would be good for execution speed and memory use.

view this post on Zulip Anton (Jul 20 2022 at 08:00):

I'm going with G; rangeIncl and rangeExcl. I feel like it would be easy to forget what the record field names are if you had to pass the boundaries as a record.

view this post on Zulip Anton (Jul 20 2022 at 08:01):

I vote A for the second poll

view this post on Zulip Anton (Jul 20 2022 at 08:02):

In the future it would be good to use numbers for polls, then we can vote with emoji :two:

view this post on Zulip Qqwy / Marten (Jul 20 2022 at 09:03):

@Anton :a: :wink:

view this post on Zulip Anton (Jul 20 2022 at 09:15):

There is no C I believe, only :a: and :b:

view this post on Zulip Anton (Jul 20 2022 at 09:15):

:c:

view this post on Zulip Qqwy / Marten (Jul 20 2022 at 09:27):

Ah, I see :sad:

view this post on Zulip Zeljko Nesic (Jul 20 2022 at 14:45):

Ah i C!

view this post on Zulip Zeljko Nesic (Jul 20 2022 at 14:46):

I'd rename a function to generateRange and gave it third parameter Bounds : [ Incl , Excl ]

view this post on Zulip Nikita Mounier (Jul 20 2022 at 15:07):

Doesn't range deserve to have its own set of operators? Something like:

1...3 == [1, 2, 3]
1..<3 == [1, 2]

This makes for-loop-style code much more ergonomic:

(1...10)
   |> List.map \_ ->
         // code

view this post on Zulip Shritesh Bhattarai (Oct 05 2022 at 16:08):

@Folkert de Vries suggested in https://github.com/roc-lang/roc/pull/4211#issuecomment-1268558719 to continue the discussion here:
I agree that we should have separate inclusive and exclusive ranges. IMO we should also provide stepping in them. The elixir ecosystem went through a breaking(-ish) change because of not providing stepping from the beginning: https://elixir-lang.org/blog/2021/05/19/elixir-v1-12-0-released/#stepped-ranges

view this post on Zulip Richard Feldman (Oct 05 2022 at 17:45):

rereading the earlier discussion, I like Brendan's proposal as a single-function design because:

List.range : { start : Int a, endsBefore : Int a, step ? Int a } -> List (Int a)

view this post on Zulip Richard Feldman (Oct 05 2022 at 17:47):

there were some discussions of having separate functions for inclusive and exclusive ranges; here's what that might look like:

List.rangeExclusive : { first : Int a, endBefore : Int a, step ? Int a } -> List (Int a)

List.rangeInclusive : { first : Int a, last : Int a, step ? Int a } -> List (Int a)

view this post on Zulip Richard Feldman (Oct 05 2022 at 17:54):

there's also the possibility of doing them without record arguments:

List.rangeExclusive : Int a, Int a -> Int a
List.rangeExclusiveBy : Int a, Int a, Int a -> Int a
List.rangeInclusive : Int a, Int a -> Int a
List.rangeInclusiveBy : Int a, Int a, Int a -> Int a

view this post on Zulip Richard Feldman (Oct 05 2022 at 17:59):

another option:

List.range : {
    start : Int a,
    end : [Before (Int a), At (Int a), Length Nat],
    step ? Int a
} -> List (Int a)

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:00):

calling that one would look like:

List.range { start: 1, end: At 10 }

List.range { start: 2, end: Before 12, step: 2 }

List.range { start: 1, end: Length (List.len myList), step: 3 }

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:01):

So my one issue with including step as an argument is that it means your start and end must always be min to max (if step defaults to 1). I think it is nice to have a generic form that can take max to min or min to max.

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:02):

oh, so I would assume you could still do max to min with a positive step

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:02):

e.g. if you're using unsigned integers

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:02):

List.range { start: 10, end: At 1 }

What does this do?

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:02):

[ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:03):

So the default step isn't 1 then? what is it?

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:03):

no, it's still 1

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:03):

but step gets subtracted if start > end

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:04):

otherwise what if you have unsigned ints for start and end? how would you ever specify decreasing by more than 1 each step?

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:04):

Ok. Then what is the difference between these:

List.range { start: 10, end: At 1, step: 1 }
List.range { start: 10, end: At 1, step: -1 }
List.range { start: 1, end: At 10, step: 1 }
List.range { start: 1, end: At 10, step: -1 }

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:04):

I guess I'm implicitly making the argument that step should be unsigned I guess :big_smile:

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:05):

otherwise what if you have unsigned ints for start and end? how would you ever specify decreasing by more than 1 each step?

would be the reverse of what you just said, make step always the signed form and then add it in. Would mean everything is signed until casted to the final value.

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:05):

ah, interesting

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:06):

that works better from a type perspective because it means you can have step be Int *

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:06):

whereas for unsigned you have to pick a specific number, which gets awkward because if you pick U128 you're paying a perf cost, but if you pick something lower, then it's not possible to have a 128-bit step :thinking:

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:07):

ok, so here's another possibility:

List.range :
    {
        start : Int a,
        end : [Before (Int a), At (Int a), Length Nat],
    }
    -> List (Int a)

List.rangeBy :
    {
        start : Int a,
        end : [Before (Int a), At (Int a), Length Nat],
    },
    Int *
    -> List (Int a)

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:08):

so List.range would always use a step of either 1 or -1 depending on whether or not start > end

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:09):

whereas List.rangeBy would add the step argument each time, whatever value you provide there

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 18:09):

I like that

view this post on Zulip Richard Feldman (Oct 05 2022 at 18:30):

what do others think?

view this post on Zulip Ayaz Hafiz (Oct 05 2022 at 18:43):

I don't have a strong opinion about this but I like it too!

view this post on Zulip jan kili (Oct 05 2022 at 18:47):

Can we move the step parameter into the record?

view this post on Zulip jan kili (Oct 05 2022 at 18:48):

And then can we combine both functions by making it an optional field?

view this post on Zulip jan kili (Oct 05 2022 at 18:49):

I love the use of tags here, and as a side note it might be the first "functionality tags" that new Roc devs are exposed to, which is a great use case for showing off their power

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:17):

We can maybe do that, but it requires making sense of all of these more directly:

List.range { start: 1, end: At 10}
List.range { start: 10, end: At 1}
List.range { start: 1, end: At 10, step: 1 }
List.range { start: 10, end: At 1, step: 1 }
List.range { start: 1, end: At 10, step: -1 }
List.range { start: 10, end: At 1, step: -1 }

My big issue is that if step is optional, it has to default to some value, theoretically 1. If it defaults to 1, that means that List.range { start: 10, end: At 1} would return []. You would always have to write List.range { start: 10, end: At 1, step: -1}.

I definitely think List.range { start: 10, end: At 1} should result in [10, 9, 8, 7, 6, 5, 4, 3, 2, 1].

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:18):

I guess you could default it to 0 and special case 0 to mean add or subtract 1 depending on what makes mores sense for the range?

view this post on Zulip Tommy Graves (Oct 05 2022 at 19:42):

Probably a silly idea, but what if you only used tags and no record?

List.range (StartAt 1) (EndAt 10)
List.range (StartAt 1) (EndBefore 10)
List.range (StartAt 10) (EndAt 1)

view this post on Zulip jan kili (Oct 05 2022 at 19:46):

@Brendan Hansknecht I don't see how moving step from a separate parameter to a record field changes the cases the function must handle.

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:50):

I guess I will focus in on the one specific issue and not the many cases. If we use an optional step that defaults to 1.

ex1: List.range { start: 1, end: At 10} -> List.range { start: 1, end: At 10, step: 1} -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
ex2: List.range { start: 10, end: At 1} -> List.range { start: 10, end: At 1, step: 1} -> []

I think ex1 is working as expected. I think ex2 is wrong. I think ex2 should output [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

view this post on Zulip jan kili (Oct 05 2022 at 19:51):

I also don't see why we'd need a "default" numerical value - why not an "inferred" numerical value? Pseudocode:

step = if start < end then 1 else -1

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:52):

I am pretty sure that is how optional record fields work. It is optional because it gets set to a specific value in the function definition. Though maybe I am wrong about optional record fields.

view this post on Zulip jan kili (Oct 05 2022 at 19:52):

Perhaps step's input type should be a tag union like [ByOne, By (Int *)] to support this

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:53):

I guess more tags would solve this.

view this post on Zulip jan kili (Oct 05 2022 at 19:54):

ALWAYS MOAR TAGS haha

view this post on Zulip jan kili (Oct 05 2022 at 19:54):

They're so dang expressive.

view this post on Zulip jan kili (Oct 05 2022 at 19:55):

@Tommy Graves I'm honestly neutral on one record parameter or multiple positionals parameters, since these tags are so expressive they don't need keys/names

view this post on Zulip jan kili (Oct 05 2022 at 19:56):

Maybe there's some other reason for/against mono-parameter functions that tilts the balance

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 19:57):

As long as the tags are generally known at compile time/the functions get inlined, tags should be just fine. In some situations, they can lead to a performance cost that gets magnified by hot loops.

view this post on Zulip jan kili (Oct 05 2022 at 20:04):

minutes = List.range (StartAt 0) (EndBefore 60) (StepBy 5) looks great :)

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 20:06):

Maybe a tad verbose, but definitely explicit. Also, without the record and optional field, you would have to write:

minutes = List.range (StartAt 0) (EndBefore 60) Default

Feels a bit odd. Though I think I am mostly bothered by the need to use parenthesis.

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 20:07):

haha

view this post on Zulip jan kili (Oct 05 2022 at 20:10):

minutes = List.range (StartAt 0) (EndBefore 60) StepByOne

view this post on Zulip jan kili (Oct 05 2022 at 20:10):

Agreed, the parentheses are meh... and always including the tag is meh...

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 20:11):

StepByOne will maybe confuse users as a name since it might be a step of 1 or a step of -1

view this post on Zulip jan kili (Oct 05 2022 at 20:12):

Here's a curveball idea - what if positive step always took you toward your endpoint?
countdown = List.range { start: At 100, end: At 0, step: By 10 } looks fine to me

view this post on Zulip jan kili (Oct 05 2022 at 20:13):

This will simplify calls for many start > end use cases, hopefully with no performance cost :fingers_crossed:

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 20:15):

so
{ start: At 100, end: At 0, step: By 10 } -> [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 00]
{ start: At 100, end: At 0, step: By -10 } -> [00, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
{ start: At 0, end: At 100, step: By 10 } -> [00, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
{ start: At 0, end: At 100, step: By -10 } -> [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 00]

view this post on Zulip jan kili (Oct 05 2022 at 20:15):

I've had this happen before, where my start and end bounds are dynamic and on an overlapping domain... and I have to add in a little logic for keeping the signedness of the step in sync with their relationship

view this post on Zulip jan kili (Oct 05 2022 at 20:17):

Oh, and here I was assuming that a "wrongly"-signed step would give you a near-infinite loop like
{ start: At 0, end: At 100, step: By -10 } -> [0, -10, -20, ... crash?

view this post on Zulip jan kili (Oct 05 2022 at 20:17):

Idk if that's desirable for anyone, maybe... And maybe it just wraps around the numberline without overflowing??

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 20:26):

I think we definitely don't want the near infinite loop. It would lead to a panic anyway from over/underflow. So in those cases, we probably would just return []. Or I guess make it relative to start/end.

view this post on Zulip jan kili (Oct 05 2022 at 20:37):

We could also just take the absolute value of the step and make it go from start to end regardless :shrug:

view this post on Zulip jan kili (Oct 05 2022 at 20:52):

Looks like we're a bigger fan of the record though? I like being able to elide the default tag value for step :)

view this post on Zulip jan kili (Oct 05 2022 at 20:53):

Idk if the At tag is necessary for the start, but it makes it more consistent

view this post on Zulip Tommy Graves (Oct 05 2022 at 20:59):

I imagined with the tag approach you would want a separate function for defining the step, so you don't have to specify anything in the default case.

Or we could buy into optional function parameters :laughing:

I also don't like the need to use parentheses -- I wish you could do

List.range StartAt 0, EndBefore 60,

to avoid the parentheses, or something like it.

List.range [StartAt 0, EndBefore 60]

:laughing:

view this post on Zulip Richard Feldman (Oct 05 2022 at 21:20):

JanCVanB said:

Idk if the At tag is necessary for the start, but it makes it more consistent

you could have start be either At or After, for symmetry with the end being At or Before - although probably nobody would ever use After in practice :big_smile:

view this post on Zulip jan kili (Oct 05 2022 at 21:31):

Was just about to suggest that :D

view this post on Zulip Richard Feldman (Oct 05 2022 at 21:31):

to me, the most important things here are:

view this post on Zulip jan kili (Oct 05 2022 at 21:33):

(I just thought of a use case for After:
countup = List.range { start: After 0, end: At 100, step: By someDynamicStep }
for things like 2, 4, 6 or 5, 10, 15)

view this post on Zulip Richard Feldman (Oct 05 2022 at 21:33):

huh, interesting!

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 21:37):

Hmm...That worked differently than I expected. With After 0, I expected the first element to always be 1. so the output would be 1, 3, 5, ... and 1, 6, 11, 16, ...

view this post on Zulip Richard Feldman (Oct 05 2022 at 21:39):

oh I was assuming After would mean "step once past this number"

view this post on Zulip Brendan Hansknecht (Oct 05 2022 at 21:44):

That does sound pretty reasonable. I guess I just wasn't expecting it. So thought it would be worth mention my immediate thought. Anyway, I think after using step makes sense and is fine.

view this post on Zulip Richard Feldman (Oct 09 2022 at 07:01):

Brendan Hansknecht said:

I guess you could default it to 0 and special case 0 to mean add or subtract 1 depending on what makes mores sense for the range?

this seems reasonable since step by 0 doesn't make any sense and is not otherwise useful :stuck_out_tongue:

view this post on Zulip Richard Feldman (Oct 09 2022 at 07:03):

so then the proposal would be:

List.range :
    {
        start : [At (Int a), After (Int a)],
        end : [At (Int a), Before (Int a), Length Nat],
        step ? Int *
    }
    -> List (Int a)

if you give a step of 0, it means "move by 1 from start towards end, whether that would mean +1 or -1" and that's what step defaults to if you don't specify it in the record

view this post on Zulip Richard Feldman (Oct 09 2022 at 07:03):

thoughts on that design?

view this post on Zulip jan kili (Oct 09 2022 at 07:10):

step ? Int a instead of *?

view this post on Zulip jan kili (Oct 09 2022 at 07:12):

This will be the clearest range function I've ever seen, in a standard library or otherwise :heart_eyes:

view this post on Zulip jan kili (Oct 09 2022 at 07:13):

and it's an inspiration for merging functionality via tags!

view this post on Zulip Brendan Hansknecht (Oct 09 2022 at 07:26):

Step maybe can't be Int a? What if I want a range of U64, but with a start greater than end, so negative step, but using the same int type would limit to only positive step.

view this post on Zulip jan kili (Oct 09 2022 at 07:34):

Very true, but if it's Int * then there's no way for the implementation to correctly cast it to add with Int a, right?

view this post on Zulip Brendan Hansknecht (Oct 09 2022 at 07:40):

Would have to be a low level? Or compiler magic? Not sure. But yeah, i don't think it could be written in current userland roc.

view this post on Zulip jan kili (Oct 09 2022 at 07:40):

(lateral thinking - if that's the only counterpoint to using Int a, that could support having positive step always point from start to end like List.range { start: At 50, end: At 10, step: 10 } == [50, 40, 30, 20, 10])

view this post on Zulip Brendan Hansknecht (Dec 04 2022 at 19:39):

I am going to try and work on a this update since it seems useful to fix for AOC. Question on boundaries: should wrapping be allowed?

what should List.range { start: At 0i8, end: At 128i8, step: -16 } return?

[0, -16, -32, -48, -64, -80, -96, -112, 128]
Or panic due to overflowing the max negative
Or [] for being invalid.

view this post on Zulip Richard Feldman (Dec 04 2022 at 20:10):

:thinking: what do other languages do?

view this post on Zulip Brendan Hansknecht (Dec 04 2022 at 20:23):

python: [] but it has infinitely sized integers and would break if it tried to generate that range.
rust: not writable with ranges
c/c++: don't have it. user manually makes it with for loop or using std::iota, i guess
js: at least from my quick googling, doesn't have it

view this post on Zulip Richard Feldman (Dec 04 2022 at 20:33):

let's do panic for now - that's the most upgradeable way because it means nobody can depend on it working a different way

view this post on Zulip Luke Boswell (Dec 10 2022 at 23:23):

I had an idea about this feature. I really love the new API, List.range {start : At 0, end : Before 10, step : 2 } its so easy to work with. However, I was wondering if this should live in Num instead, I feel like it's actually a Num.range not a List.range.

view this post on Zulip Richard Feldman (Dec 10 2022 at 23:30):

interesting! What do others think?

view this post on Zulip Folkert de Vries (Dec 10 2022 at 23:35):

I've always thought of this as a list thing, not a number thing

view this post on Zulip Folkert de Vries (Dec 10 2022 at 23:36):

Num.range makes more sense (to me) for what rust does, where a Range<usize> is really its own type, not just a vector with the numbers in some range

view this post on Zulip Kevin Gillette (Dec 11 2022 at 01:25):

When first skimming the stdlib docs, I originally thought List.range was going to provide a subsclice operation: "give me the elements from this list that fall within this range".

Almost everything else in List takes a list as input.

view this post on Zulip Kevin Gillette (Dec 11 2022 at 01:28):

fwiw, List.sublist would benefit from the same tag style approach that range was given

view this post on Zulip Anton (Dec 11 2022 at 09:20):

I think List.range may be easier to find for beginners when searching. If you want to produce a list of numbers, I'd go looking in the list docs.

view this post on Zulip Pit Capitain (Dec 12 2022 at 12:14):

What would be the criteria for putting functions into a module? It can't be the return type alone...

view this post on Zulip Kevin Gillette (Dec 12 2022 at 14:21):

For Roc, it largely seems to be the input type (see Num.toStr vs Str.toU128). iow Num is what you use when you already have a Num, and Str is what you use when you already have a Str.

Auxiliary cases, like List.single, don't follow that rule, but there also isn't a OneOfSomething module to attach it to :wink:

If this becomes a wider topic of conversation, it'd certainly be worth its own thread.

view this post on Zulip Folkert de Vries (Dec 12 2022 at 14:23):

also you have to consider dependencies between modules. So the Str module can use the numeric types, but the Num module cannot use Str because that would create a cyclic dependency

view this post on Zulip Kevin Gillette (Dec 17 2022 at 00:51):

Should the Length tag which range accepts be Len instead, in order to align with List.len ?

view this post on Zulip Brendan Hansknecht (Dec 17 2022 at 02:05):

Yeah, that sounds like a great idea. I mix that up every time I use the function


Last updated: Jun 16 2026 at 16:19 UTC