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. On Mon, Mar 22, 2021 at 1:38 PM José Valim <jose.va...@dashbit.co> wrote: > 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 > > > On Mon, Mar 22, 2021 at 12:52 PM Amos King <a...@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.va...@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. >> >> >> >> On Mon, Mar 22, 2021 at 12:16 PM Wiebe-Marten Wijnja <w...@resilia.nl> >> wrote: >> >>> 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 >>> On 22-03-2021 11:06, José Valim wrote: >>> >>> Note: You can also read this proposal in a gist >>> <https://gist.github.com/josevalim/da8f1630e5f515dc2b05aefdc5d01af7>. >>> >>> 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. >>> >>> >>> -- >>> 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-core+unsubscr...@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 >>> <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-core+unsubscr...@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-core+unsubscr...@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-core+unsubscr...@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> >> . >> > -- 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-core+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JL-Ge-c5LF%3DH5pCiGeDwug1Nt6c-fMzuxGizaeh_%2BECA%40mail.gmail.com.