Crystal: Allow Float Ranges to be converted to array

Created on 20 Jul 2016  路  11Comments  路  Source: crystal-lang/crystal

In Ruby one can do it by means of calling _step_ method in between of initialization and calling #to_a:

(1.0..10.0).step(0.2).to_a # => [1.0, 3.0, 5.0, 7.0, 9.0]

In Crystal this throws:

Error in line 1: no overload matches 'Range(Float64, Float64)#step' with type Float64
Overloads are:
 - Range(B, E)#step(n = 1, &block)
 - Range(B, E)#step(n : Int = 1)
feature stdlib

Most helpful comment

My first reaction from just seeing the title: "don't mess with floats like that - they're always waiting for a reason to explode".

All 11 comments

Maybe. I feel like Ruby is cheating here, or introducing special cases. Here nothing about a Range is used: the succ method that defines what a Range is and how it works is not used at all. It's merely a convenience. For example you can write the above with this equivalent code:

1.0.step(by: 2, limit: 10.0).to_a # => [1.0, 3.0, 5.0, 7.0, 9.0]

There's a tiny difference when you need the range to be inclusive.

Ruby has many more special cases like these. For example:

p ("a".."bc").to_a #=> ["a", "b", ..., "ba", "bb", "bc"]

However, if you invoke succ starting from "a", you pass through "c", which is greater than "bc", so the iteration should stop... but Ruby continues, because it takes this as a special case.

In general I dislike special cases, but we _could_ introduce them if there's a strong pragmatic reason to it.

Note that I don't have anything against these special cases except that they require extra documentation and add more code to the standard library, which makes understanding this code harder. But it can be done, and I think it won't affect runtime performance because these checks are eventually removed by the compiler.

I'll try to do it :-)

(though it might take me some time)

Alternatively, if someone wants to do this, PR are welcome. I'd implement this for the block version and non-block version of step.

@asterite Thanks for your solution, that's exactly what I was looking for! 馃憤 Still, compatibility with Ruby in this regard would be a welcomed edition since it lowers entrance barriers, and I'd say produce more semantic code.

Wait what

p 0.6.step(by: 0.1, limit: 1.4).to_a
[0.6, 0.7, 0.8, 0.9, 0.0, 1.1, 1.2, 1.3]

https://carc.in/#/r/16l9

What I was gonna say, it's dangerous to keep incrementing a _float_, I would bet that Ruby does this operation like x = start + i*by instead of x += by.

But what I found was weirder.

@BlaXpirit gets even weirder here: https://carc.in/#/r/16lc

@Sija that's just the way floats work unfortunately, they can't represent every decimal in true precision.

My first reaction from just seeing the title: "don't mess with floats like that - they're always waiting for a reason to explode".

codetriage

Running the original code on crystal 0.21.1 produces a different stacktrace than originally reported:

$ crystal run 3021.cr --error-trace
Error in 3021.cr:1: instantiating 'Range::StepIterator(Range(Float64, Float64), Float64, Float64)#to_a()'

(1.0..10.0).step(0.2).to_a
                      ^~~~

in /usr/local/Cellar/crystal-lang/0.21.1_1/src/enumerable.cr:1278: instantiating 'each()'

    each { |e| ary << e }
    ^~~~

in /usr/local/Cellar/crystal-lang/0.21.1_1/src/iterator.cr:384: instantiating 'Range::StepIterator(Range(Float64, Float64), Float64, Float64)#next()'

      value = self.next
                   ^~~~

in /usr/local/Cellar/crystal-lang/0.21.1_1/src/range.cr:356: undefined method 'times' for Float64

        @step.times { @current = @current.succ }
              ^~~~~

================================================================================

Float64 trace:

  /usr/local/Cellar/crystal-lang/0.21.1_1/src/range.cr:348

        def initialize(@range, @step, @current = range.begin, @reached_end = false)
                               ^~~~

  /usr/local/Cellar/crystal-lang/0.21.1_1/src/range.cr:348

        def initialize(@range, @step, @current = range.begin, @reached_end = false)
                               ^

This indicates that the resulting class is trying to call times on something that doesn't have this method.

The Int type appears to be the only type that has times methods: https://github.com/crystal-lang/crystal/blob/9e67166a5760428bdc7a8f519341529c23c3698f/src/int.cr#L331-L341

For record, Ruby doesn't have a times on the Float class either.

Considering the workarounds published, should this issue be closed?

@miketheman agreed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

RX14 picture RX14  路  3Comments

pbrusco picture pbrusco  路  3Comments

jhass picture jhass  路  3Comments

asterite picture asterite  路  3Comments

costajob picture costajob  路  3Comments