Ranges with steps

185 views
Skip to first unread message

José Valim

unread,
Mar 22, 2021, 6:06:34 AM3/22/21
to elixir-l...@googlegroups.com
Note: You can also read this proposal in a gist.

This is a proposal to address some of the limitations we have in Elixir ranges today. They are:

  * It is not possible to have ranges with custom steps
  * It is not possible to have empty ranges
  * Users may accidentally forget to check the range boundaries

The first limitation is clear: today our ranges are increasing (step of 1) or decreasing (step of -1), but we cannot set arbitrary steps as in most other languages with range. For example, we can't have a range from 1 to 9 by 2 (i.e. 1, 3, 5, 7, 9).

The second limitation is that, due to how we currently infer the direction of ranges, it is not possible to have empty ranges. Personally, I find this the biggest limitation of ranges. For example, take the function `Macro.generate_arguments(n, context)` in Elixir. This is often used by macro implementations, such as `defdelegate`, when it has to generate a list of `n` arguments. One might try to implement this function as follows:

```elixir
def generate_arguments(n, context) do
  for i <- 1..n, do: Macro.var(:"arg#{n}", context)
end
```

However, because `n` may be zero, the above won't work: for `n = 0`, it will return a list with two elements! To workaround this issue, the current implementation works like this:

```elixir
def generate_arguments(n, context) do
  tl(for i <- 0..n, do: Macro.var(:"arg#{n}", context))
end
```

In other words, we have to start the range from 0 and always discard the first element which is unclear and wasteful.

Finally, another issue that may arise with ranges is that implementations may forget to check the range boundaries. For example, imagine you were to implement `range_to_list/1`:

```elixir
def range_to_list(x..y), do: range_to_list(x, y)
defp range_to_list(y, y), do: [y]
defp range_to_list(x, y), do: [x | range_to_list(x + 1, y)]
```

While the implementation above looks correct at first glance, it will loop forever if a decreasing range is given.

## Solution

My solution is to support steps in Elixir ranges by adding `..` as a ternary operator. The syntax will be a natural extension of the current `..` operator:

```elixir
start..stop..step
```

Where `..step` is optional. This syntax is also available in F#, except F# uses:

```elixir
start..step..stop
```

However, I propose for step to be the last element because it mirrors an optional argument (and optional arguments in Elixir are typically last).

The ternary operator solves the three problems above:

> It is not possible to have ranges with steps

Now you can write `1..9..2` (from 1 to 9 by 2).

> It is not possible to have empty ranges

This can be addressed by explicitly passing the step to be 1, instead of letting Elixir infer it. The `generate_arguments` function may now be implemented as:

```elixir
def generate_arguments(n, context) do
  for i <- 1..n..1, do: Macro.var(:"arg#{n}", context)
end
```

For `n = 0`, it will construct `1..0..1`, an empty range.

Note `1..0..1` is distinct from `1..0`: the latter is equal to `1..0..-1`, a decreasing range of two elements: `1` and `0`. To avoid confusion, we plan to deprecate inferred decreasing ranges in the future.

> Users may accidentally forget to check the range boundaries

If we introduce ranges with step and the ternary operator, we can forbid users to write `x..y` in patterns. Doing so will emit a warning and request them to write `x..y..z` instead, forcing them to explicitly consider the step, even if they match on the step to be 1. In my opinion, this is the biggest reason to add the ternary operator: to provide a convenient and correct way for users to match on ranges with steps.

## Implementation

The implementation happens in three steps:

  1. Add `..` as a ternary operator. `x..y..z` will have the AST of `{:.., meta, [x, y, z]}`

  2. Add the `:step` to range structs and implement `Kernel.".."/3`

  3. Add deprecations. To follow Elixir's deprecation policy, the deprecation warnings shall only be emitted 4 Elixir versions after ranges with steps are added (most likely on v1.16):

      * Deprecate `x..y` as a shortcut for a decreasing range in favor of `x..y..-1`. The reason for this deprecation is because a non-empty range is more common than a decreasing range, so we want to optimize for that. Furthermore, having a step with a default of 1 is clearer than having a step that varies based on the arguments. Of course, we can only effectively change the defaults on Elixir v2.0, which is still not scheduled or planned.

      * Deprecate `x..y` in patterns, require `x..y..z` instead. This will become an error on Elixir v2.0.

      * Deprecate `x..y` in guards unless the arguments are literals (i.e. `1..3` is fine, but not `1..y` or `x..1` or `x..y`). This is necessary because `x..y` may be a decreasing range and there is no way we can warn about said cases in guards, so we need to restrict at the syntax level. For non-literals, you should either remove the range or use an explicit step. On Elixir v2.0, `x..y` in guards will always mean a range with step of 1.


Wiebe-Marten Wijnja

unread,
Mar 22, 2021, 7:16:36 AM3/22/21
to elixir-l...@googlegroups.com

As someone who has encountered quite a number of situations in which an empty range would have been useful, I am very excited by this proposal!


Two questions:

1. What about using a different syntax for separating the third parameter?

If there is any way to make it more obvious that the third parameter is the step rather than the (upper) bound, then in my opinion this might be preferable over having syntax which is e.g. "just like F#'s but with opposite meaning". The less ambiguous we can make it (for people coming from other languages, and for people in general), the better.
Maybe `1..10:3`?

2. What will the step-based syntax expand to in guards?

`when foo in 42..69` expands  to `when is_integer(foo) and foo >= 42 and foo <= 69`.
What should `when foo in 42..69..3` (again assuming x, y, z to be literals) expand to?
Maybe `when is_integer(foo) and foo >= 42 and foo <= 69 and rem(foo - 42), 3)`?
Or is there a better alternative?


~Marten / Qqwy

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BxGUW-nBj0qqRygR_-J05c05bW6mpDV9ki-HPCvfrudQ%40mail.gmail.com.
OpenPGP_signature

José Valim

unread,
Mar 22, 2021, 7:32:22 AM3/22/21
to elixir-l...@googlegroups.com
> 1. What about using a different syntax for separating the third parameter?

Suggestions are welcome. The proposed x..y:z doesn't work though, since y/z can be taken to mean keyword or an atom. And, FWIW, I didn't take x..y..z because of F#, but rather as a natural extension of .. that at least exists elsewhere too. It is important to not confuse the cause here. :)

> 2. What will the step-based syntax expand to in guards? Maybe `when is_integer(foo) and foo >= 42 and foo <= 69 and rem(foo - 42), 3)`?

Correct.



Amos King

unread,
Mar 22, 2021, 7:52:48 AM3/22/21
to elixir-l...@googlegroups.com
What about something closer to Haskell’s ranges? [first, second..last] is their syntax and the step in inferred by the difference between first and second. 1..2..n would step by one. 1..3..n is step by two. 1..2..0 would be empty, etc.

Negative steps. 1..0..-10. 1..0..10 would return an empty range.

I like this syntax because it creates an interesting logical thought as I how I’m counting. I think it is a friendlier syntax that doesn’t have to be explained in as much detail. 1..n makes sense when I look at it. 1..-1 also makes sense at a glance. 1..2..10 makes sense IMO. 1..10..2 looks surprising and confusing to me. 

Amos

On Mar 22, 2021, at 06:32, José Valim <jose....@dashbit.co> wrote:



Adam Lancaster

unread,
Mar 22, 2021, 8:16:35 AM3/22/21
to elixir-l...@googlegroups.com
One thought I have (which I don't know is helpful or not)

I'm wondering if a "step" in a range should be a function instead?

If we ignore the syntax sugar for now and think of a range as a stream of values with a start, a stop and a function that determines how to get the next value, then this would make sense to me:

stepper = fn previous_value -> previous_value + 1 end
Range.new(1, 10, stepper)

In which case maybe a sigil is a better approach to the syntax?

I feel like this approach may also allow ranges of letters for example:

Range.new("a", "b", fn letter -> ?letter + 1 end)

Best

Adam



jean.pa...@gmail.com

unread,
Mar 22, 2021, 8:20:57 AM3/22/21
to elixir-l...@googlegroups.com
+1 for first..second..last


Jean

Andrew Timberlake

unread,
Mar 22, 2021, 8:37:18 AM3/22/21
to elixir-l...@googlegroups.com
I agree that 2..4..10 seems more intuitive than 2..10..2
In the first example both “..” imply “to”. In the second example the first “..” means “to” and the second “..” now means “by”.
2..4...10 might better communicate onward to 10.

Andrew
—sent from my iPhone


On 22 Mar 2021, 13:52 +0200, Amos King <am...@binarynoggin.com>, wrote:
What about something closer to Haskell’s ranges? [first, second..last] is their syntax and the step in inferred by the difference between first and second. 1..2..n would step by one. 1..3..n is step by two. 1..2..0 would be empty, etc.

Negative steps. 1..0..-10. 1..0..10 would return an empty range.

I like this syntax because it creates an interesting logical thought as I how I’m counting. I think it is a friendlier syntax that doesn’t have to be explained in as much detail. 1..n makes sense when I look at it. 1..-1 also makes sense at a glance. 1..2..10 makes sense IMO. 1..10..2 looks surprising and confusing to me. 

Amos

On Mar 22, 2021, at 06:32, José Valim <jose....@dashbit.co> wrote:


> 1. What about using a different syntax for separating the third parameter?

Suggestions are welcome. The proposed x..y:z doesn't work though, since y/z can be taken to mean keyword or an atom. And, FWIW, I didn't take x..y..z because of F#, but rather as a natural extension of .. that at least exists elsewhere too. It is important to not confuse the cause here. :)

> 2. What will the step-based syntax expand to in guards? Maybe `when is_integer(foo) and foo >= 42 and foo <= 69 and rem(foo - 42), 3)`?

Correct.



--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BX2CPbHsMgM0vMOpmV%2BjvE26r%2Bw-%2BmafnQC5i-G8Qspg%40mail.gmail.com.
--

You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

José Valim

unread,
Mar 22, 2021, 8:39:07 AM3/22/21
to elixir-l...@googlegroups.com
Hi Amos, I considered the Haskell approach, but the issue is really pattern matching:

What should

x..y..z = range

match on?

If we want to keep creation and matching consistenting, then it has to be first..second..last, which means everyone now has to compute the step. It also means checking if it is an increased range or decreasing range is more verbose too, we always have to do: y - x > 0, as well as the guard checks.

Therefore, if we want to go down this route, we need to accept the following trade-offs:

1. x..y and x..y..z won't be allowed in patterns (you will have to match on %Range{})

2. We need to manually compute the steps by hand in almost all range operations


José Valim

unread,
Mar 22, 2021, 8:49:27 AM3/22/21
to elixir-l...@googlegroups.com
FWIW, the reason why Haskell doesn't have those trade-offs is because, afaik, the syntax is really a shortcut for a lazy list. This doesn't work for us because Range is really a specific data type that we want to introspect. So we need to represent the data in a way that is good for both.

> I'm wondering if a "step" in a range should be a function instead?

A function cannot be invoked in guards. So that rules it out. It has to be an integer.

Stefan Chrobot

unread,
Mar 22, 2021, 8:56:54 AM3/22/21
to elixir-l...@googlegroups.com
I have a feeling this would be better addressed with two syntax constructs.
Isn't the issue of empty ranges really an issue of the range being right-closed without a way to say it should be right-open? So x..y.. could mean from x to y, excluding y.
I think the step part would be better addressed with something like x..y|z (or maybe \\ instead of |).

eksperimental

unread,
Mar 22, 2021, 8:57:03 AM3/22/21
to elixir-l...@googlegroups.com
> 1. x..y and x..y..z won't be allowed in patterns (you will have to
> match on %Range{})

José, wouldn't this be a backward incompatible introduction? Even if we
still soft deprecated and emit a warning, there could be a lot of
changes to do, since this is not just a function call that needs to be
updated, but a syntax one.

Have you considered making a this a new data type with its own syntax?
For example:
1...100...2
> >> <https://gist.github.com/josevalim/da8f1630e5f515dc2b05aefdc5d01af7>.
> >> <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BxGUW-nBj0qqRygR_-J05c05bW6mpDV9ki-HPCvfrudQ%40mail.gmail.com?utm_medium=email&utm_source=footer>
> >> .
> >>
> >> --
> >> You received this message because you are subscribed to the Google
> >> Groups "elixir-lang-core" group.
> >> To unsubscribe from this group and stop receiving emails from it,
> >> send an email to elixir-lang-co...@googlegroups.com.
> >> To view this discussion on the web visit
> >> https://groups.google.com/d/msgid/elixir-lang-core/e1f904b3-3cd2-0ef1-f438-8408f5102c48%40resilia.nl
> >> <https://groups.google.com/d/msgid/elixir-lang-core/e1f904b3-3cd2-0ef1-f438-8408f5102c48%40resilia.nl?utm_medium=email&utm_source=footer>
> >> .
> >>
> > --
> > You received this message because you are subscribed to the Google
> > Groups "elixir-lang-core" group.
> > To unsubscribe from this group and stop receiving emails from it,
> > send an email to elixir-lang-co...@googlegroups.com.
> > To view this discussion on the web visit
> > https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BX2CPbHsMgM0vMOpmV%2BjvE26r%2Bw-%2BmafnQC5i-G8Qspg%40mail.gmail.com
> > <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BX2CPbHsMgM0vMOpmV%2BjvE26r%2Bw-%2BmafnQC5i-G8Qspg%40mail.gmail.com?utm_medium=email&utm_source=footer>
> > .
> >
> > --
> > You received this message because you are subscribed to the Google
> > Groups "elixir-lang-core" group.
> > To unsubscribe from this group and stop receiving emails from it,
> > send an email to elixir-lang-co...@googlegroups.com.
> > To view this discussion on the web visit
> > https://groups.google.com/d/msgid/elixir-lang-core/7F881DB7-5E72-4DEC-AE89-9558E72E253F%40binarynoggin.com
> > <https://groups.google.com/d/msgid/elixir-lang-core/7F881DB7-5E72-4DEC-AE89-9558E72E253F%40binarynoggin.com?utm_medium=email&utm_source=footer>
> > .
> >
>

José Valim

unread,
Mar 22, 2021, 9:09:32 AM3/22/21
to elixir-l...@googlegroups.com
> Isn't the issue of empty ranges really an issue of the range being right-closed without a way to say it should be right-open? So x..y.. could mean from x to y, excluding y.

You could frame the issue as open/closed ranges but, given our ranges are exclusive to integers, the general consensus is to treat integer ranges as closed because you can flip between them using +1 and -1 (the a..b notation is also used in math). So I would prefer to approach the problem using steps, because it is strictly more expressive.

> José, wouldn't this be a backward incompatible introduction?

All code written today will still work. However, if you use the new features (empty ranges or custom steps), then old code may not process the range accordingly. So it is backwards compatible, as all of the code written today still works. You may run into issues only if you use new features. It is definitely something to keep in mind though.

Amos King - Binary Noggin

unread,
Mar 22, 2021, 9:10:42 AM3/22/21
to elixir-l...@googlegroups.com
I didn't think about the matching pulling `first, second, last` and then having to figure out the step if that is what you wanted to know. Good catch.

I still find the syntax to be confusing with the step as the last element. I really wish that we could do something like `a..b by: 3` but that comes with other implementation issues. I like the proposals using a different operator for the step. `a..b\\c`?

Amos King, CEO

      

573-263-2278 am...@binarynoggin.com




eksperimental

unread,
Mar 22, 2021, 9:22:40 AM3/22/21
to elixir-l...@googlegroups.com
On Mon, 22 Mar 2021 08:10:29 -0500
Amos King - Binary Noggin <am...@binarynoggin.com> wrote:

> I like the proposals using a different operator for the step.
> `a..b\\c`?

+1 on this one. Visually it is less confusing to have
`1..100\\2`
than to have
`1..100..2`
as it is easier to identify the range part, plus it keeps the same
syntax for the current ranges, whereas the `100..2` part could be
visually mistaken as a range.

eksperimental

unread,
Mar 22, 2021, 9:24:54 AM3/22/21
to elixir-l...@googlegroups.com
On Mon, 22 Mar 2021 14:09:17 +0100
José Valim <jose....@dashbit.co> wrote:

> > José, wouldn't this be a backward incompatible introduction?
>
> All code written today will still work. However, if you use the new
> features (empty ranges or custom steps), then old code may not
> process the range accordingly. So it is backwards compatible, as all
> of the code written today still works. You may run into issues only
> if you use new features. It is definitely something to keep in mind
> though.

Sorry I read your answer to Amos, and I thought your original proposal
would require to pattern match on structs, but you were referring to
his approach.

sabi...@gmail.com

unread,
Mar 22, 2021, 9:48:42 AM3/22/21
to elixir-lang-core
Really excited about this proposal too!


> However, I propose for step to be the last element because it mirrors an optional argument (and optional arguments in Elixir are typically last).

Even if this is "typical", there seems to be some exceptions when it makes more sense API-wise (e.g. `Enum.find/3` or `Enum.map_join/3 have a default argument in the middle, I assume to have the `fun` as a last param).
I think that the `start..step..stop` syntax might not be in contradiction with anything and might feel more natural in this case, while still covering all of the other points?

But the original proposal looks good as well :)

José Valim

unread,
Mar 22, 2021, 10:05:49 AM3/22/21
to elixir-l...@googlegroups.com
 
I still find the syntax to be confusing with the step as the last element. I really wish that we could do something like `a..b by: 3` but that comes with other implementation issues. I like the proposals using a different operator for the step. `a..b\\c`?

Unfortunately a..b\\c is ambiguous because \\ is used as default arguments. So you could do "a..b\\1..3". It is semantically unambiguous in this case, but definitely syntactically ambiguous.

Here are some approaches of what we could allow. I am considering they all represent the range from 1 to 9 by 2 and from 9 to 1 by -1:
  • 1..2..9 and 9..-1..1 - as someone proposed, maybe having the step in the middle is clearer

  • 1..9//2 and 9..1//-1 - note // is generally ambiguous in Elixir because of the capture operator. So this will only work if ..// is defined as a ternary operator. This means we will likely introduce the atom :..// and the capture operator would be &..///3. I think those are acceptable trade-offs, but worth mentioning.

  • Combinations with \ and /:
    • 1..9/\2 and 9..1/\-1
    • 1..9*\2 and 9..1*\-1
    • 1..9\/2 and 9..1\/-1
    • 1..9*/2 and 9..1*/-1

  • 1..9^^2 and 9..1^^-2
To be honest, I like the first two. One nice bonus about 1..9//2 is because we can actually think that it is cutting the number of elements in 2, by some approximation. 1..8//1 has 8 elements. 1..8//2 has 4. :)

Amos King - Binary Noggin

unread,
Mar 22, 2021, 10:26:13 AM3/22/21
to elixir-l...@googlegroups.com
Yes, the `1..9//2` communicates intention a little better than `a..b..c` IMO.

Amos King, CEO

      

573-263-2278 am...@binarynoggin.com



--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Ben Wilson

unread,
Mar 22, 2021, 11:00:25 AM3/22/21
to elixir-lang-core
The 1..9//2 structure feels like the best of the presented options to me. I think it reads well out loud, since .. often means "to" and / is often rendered as "by" so that would read 1 to 9 by 2.

To make sure I'm clear about the semantics: If I have:

```
1..x//1 |> Enum.to_list
```

Then if `x` is zero or less than zero, I get an empty list? This would definitely be nice, we've been bitten by the exact scenarios mentioned in the first post.

José Valim

unread,
Mar 22, 2021, 11:12:45 AM3/22/21
to elixir-l...@googlegroups.com
> Then if `x` is zero or less than zero, I get an empty list? This would definitely be nice, we've been bitten by the exact scenarios mentioned in the first post.

Precisely.

I will change the proposal in a couple minutes to use x..y//z. Thanks Amos for pushing into this direction. <3

Anil Kulkarni

unread,
Mar 22, 2021, 11:57:52 AM3/22/21
to elixir-l...@googlegroups.com
Generally I like the proposal, especially 1..0 returning an empty list.

Since we're bike shedding on the syntax, I would throw my hat into the ring for
1..10:2

Reasons:
1) : is used in many languages as part of the ?: ternary operator. (I know elixir doesn't have it but maybe in the future :D) so if ?: is ternary for branch then ..: is ternary for range and you could continue the approach for other syntax in the future.
2) \\ means optional right now but in practice we don't really want this to be optional in the future, correct? E.g. If new docs/syntax are going to say always do 1..10\\2 then the optional meaning is lost to me
3) minor: I always get \\ and // mixed up so it's a harder syntax personally to remember.

Thanks,
Anil






-------- Original Message --------
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JNXVDdJG%3DJ2MuiiN9%2BxHNUm58%2Bi%3DAbPygGkZm6sZ4jEA%40mail.gmail.com.

José Valim

unread,
Mar 22, 2021, 12:46:16 PM3/22/21
to elixir-l...@googlegroups.com
 
Since we're bike shedding on the syntax, I would throw my hat into the ring for
1..10:2

It was commented before but the syntax above is ambiguous due to the use of atoms and keywords, for example: x..y:z.

Christopher Keele

unread,
Mar 22, 2021, 8:31:52 PM3/22/21
to elixir-lang-core
I like the idea of supporting steps in ranges in general! The path to supporting it in range literals with special rules for guard, match, and normal contexts seems a little rough, but I can't think of a better way than what's been proposed so far.

x..y//z seems like a reasonable syntax to me. "One nice bonus about 1..9//2 is because we can actually think that it is cutting the number of elements in 2, by some approximation"  — agreed, is has a nice divisible/modulo feel to it.

————————

I know that doing this work just to interpret x..y when x > y as an empty range is not the only motivator of this proposal, but a contributing factor. This is specifically for the bounds-checking errors in the common scenario x..y when is_literal(x) and is_variable(y), right? Specifically:

for i <- 1..n, do: # zero times when n = 0


 This feels like we're hacking around the lack of a notion of inclusivity/exclusivity; have we considered syntax around that instead of/as well as step notation?

For example, using a comma "," to indicate an exclusive (rather the inclusive ".") boundary of a range. In the common case above, it seems like we could more accurately model the programmer's real intent: defining a dynamic zero-exclusive range; ex:

for i <- 0,.n, do: # zero times when n = 0

I believe we could still make this syntax work with all the compile-time, pattern-matching, and guard-expressivity that ranges have today.

0,.0 -> []
0,.1 -> [1]
2.,4 -> [2, 3]
2,,4 -> [3]

Not to derail the step discussion—but it does seem like on-topic insofar as the motivations of the step feature is concerned?

José Valim

unread,
Mar 23, 2021, 2:27:14 AM3/23/21
to elixir-l...@googlegroups.com
> The path to supporting it in range literals with special rules for guard, match, and normal contexts seems a little rough, but I can't think of a better way than what's been proposed so far.

To be clear, that's already how it works today. We have special rules for these contexts. The only addition is that we will deprecate x..y in match.

>  This feels like we're hacking around the lack of a notion of inclusivity/exclusivity; have we considered syntax around that instead of/as well as step notation?

I have commented on this earlier. My comment was: "You could frame the issue as open/closed ranges but, given our ranges are exclusive to integers, the general consensus is to treat integer ranges as closed because you can flip between them using +1 and -1 (the a..b notation is also used in math). So I would prefer to approach the problem using steps, because it is strictly more expressive." I would also add that languages like Julia and Matlab also have inclusive ranges with steps.

Inclusivity/exclusivity was actually the first idea I explored, especially coming from Ruby, but no dice . :) The other issue with inclusivity and exclusivity is that we can't make it work in guards. We have two options:

* Store the exclusivity as a field in the range - but how are we going to remind users implementing range_to_list to check this field?

* Keep ranges as is and normalize the boundary when creating the range - this means you can't use x,.y in match, because we can't have a x-n,.y in matches, where the value of n depends on the direction of the range

So inclusivity/exclusivity solves only part of the problems originally listed.


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

José Valim

unread,
Mar 24, 2021, 5:55:16 AM3/24/21
to elixir-l...@googlegroups.com
I have sent a PR with an implementation of the proposal: https://github.com/elixir-lang/elixir/pull/10810

Some notes so far:

* I am really happy with the x..y//z (I am aware I am biased :D). Going for different separators between limits and steps was a great call. first..last is the mathematical notation and first..last//step feels like a natural extension

* This pull request also allows us to add interesting new features to both Enum.slice/String.slice, such as slicing in steps or even reversing+slicing. However, I am not exploring those features right now.

Christopher Keele

unread,
Mar 24, 2021, 12:22:52 PM3/24/21
to elixir-lang-core
> I am really happy with the x..y//z

I just played around with this and agree, it is much nicer than x..y..z,!

Kevin Johnson

unread,
Apr 6, 2021, 7:48:14 PM4/6/21
to elixir-l...@googlegroups.com

Has there been any discussion about unbounded ranges? For instance in Haskell: 
`take 24 [13,26..]`

In our case the equivalent would be:
`13..//13 |> Enum.take(24)`

or perhaps for the sake of being more explicit:
`13..∞//13`

Austin Ziegler

unread,
Apr 6, 2021, 8:18:03 PM4/6/21
to elixir-l...@googlegroups.com
Wouldn’t it be better to use `Stream.iterate/2` than try to embed this
into a data structure?

`Stream.iterate(13, & &1 + 13) |> Enum.take(24)`

It would take a little more effort to support front-unbounded, but
that would be something more like:

`Stream.iterate(-∞, & &1 + 13) |> Enum.take_while(& &1 <= 31331) |>
Enum.take(256)`

That would be `..31331`, I think.

Assuming that `∞` is usefully defined as infinity that can be negated
(it isn’t right now).

More generally, ranges in Elixir are _extremely_ limited as they are
abstractions of integer sequences and not continuous ranges as they
might be in other languages (e.g., Ruby), so it’s not possible to ask
whether a range covers a value. (I still haven’t figured out what I’d
use an endless range for in Ruby, but that’s just me.)

-a
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAAkfbUpJcM%2B-FEirXrj1bX4CYD7ciMe80vYh64p_09UQ7-1D0A%40mail.gmail.com.



--
Austin Ziegler • halos...@gmail.comaus...@halostatue.ca
http://www.halostatue.ca/http://twitter.com/halostatue

Kevin Johnson

unread,
Apr 6, 2021, 8:55:53 PM4/6/21
to elixir-l...@googlegroups.com
Wouldn’t it be better to use `Stream.iterate/2` than try to embed this
into a data structure?
It is certainly possible, but would the same argument not also apply to the original proposal where for-comprehension could be resorted to instead?

IMHO, a range(together with its now established stepping capability) is but syntactic sugar. (Syntactic sugar causes cancer of the semi-colons though!!!)

I feel a strong case can be made to extend this syntactic sugar to a Stream, which is also but an enumerable at the end of the day.
The conciseness of `13..∞//13` without sacrificing readability I would say is very appealing here. (Does anyone feel differently about this?)

Besides readability, is there another concern I fail to see on account of which it is best to stick with a `Stream.iterate`?

Christopher Keele

unread,
Apr 6, 2021, 9:36:35 PM4/6/21
to elixir-lang-core
> IMHO, a range(together with its now established stepping capability) is but syntactic sugar

This is true, but in Elixir a range is syntactic sugar specifically for a compile-time list of integers. This is what lets it be used in guards and pattern matching, where streams (and non-literal enumerables, and comprehensions) cannot be employed.

So ranges are meant for bounded compile-time integer sequences, stream is useful for building lazy infinite runtime ones, and the two have very different intended usecases. :)

Kevin Johnson

unread,
Apr 6, 2021, 11:20:52 PM4/6/21
to elixir-l...@googlegroups.com
This is true, but in Elixir a range is syntactic sugar specifically for a compile-time list of integers. This is what lets it be used in guards and pattern matching, where streams (and non-literal enumerables, and 
comprehensions) cannot be employed.
So ranges are meant for bounded compile-time integer sequences, stream is useful for building lazy infinite runtime ones, and the two have very different intended usecases. :)

The crux of what you are explaining boils down to the fact that until now, compile time, ranges have been afforded special treatment
If we really wanted to, we could make streams work in guard clauses as well.
For instance, consider the infinite sequence of even numbers: 
```
even_numbers = 0..∞//2
```

That above sequence can be translated to a guard clause as:
```
defguard is_even(n) when rem(n, 2) == 0
```

If we want this to work:
```
def eval(n) when n in 0..∞//2, do: true
``` 

then we only need to have a way of linking that `defguard` clause above with this infinite range. 
This most certainly will require some careful thought how you link them, but for now it may be plausible to expand the definition of the struct Range to encompass a module to delegate to where the infinite sequence is translated appropriately to an explicit guard clause.

Just like currently the compiler is doing extra work to facilitate:
```
def eval(n) when n in [0, 2, 4, 6, 8, 10], do: true
```
likewise, there should be no fundamental objection to accommodate an infinite stream in a guard clause.

This is what lets it be used in guards and pattern matching 
Which scenario can you pattern match a range? For instance in `iex` this fails: `[x | _] = 1..5`. Is there any compile time example you can share?

So ranges are meant for bounded compile-time integer sequences
This is not entirely true as you can compose a range dynamically:
```
def rnd(min, max), do: Enum.random(min..max)
``` 



Reply all
Reply to author
Forward
0 new messages