10

I need to implement a for loop that goes from one floating point number to another with the step as another floating point number.

I know how to implement that in C-like languages:

for (float i = -1.0; i < 1.0; i += 0.01) { /* ... */ }

I also know that in Rust I can specify the loop step using step_by, and that gives me what I want if I have the boundary values and step as integers:

#![feature(iterator_step_by)]

fn main() {
    for i in (0..30).step_by(3) {
        println!("Index {}", i);
    }
}

When I do that with floating point numbers, it results in a compilation error:

#![feature(iterator_step_by)]

fn main() {
    for i in (-1.0..1.0).step_by(0.01) {
        println!("Index {}", i);
    }
}

And here is the compilation output:

error[E0599]: no method named `step_by` found for type `std::ops::Range<{float}>` in the current scope
--> src/main.rs:4:26
  |
4 |     for i in (-1.0..1.0).step_by(0.01) {
  |                          ^^^^^^^
  |
  = note: the method `step_by` exists but the following trait bounds were not satisfied:
          `std::ops::Range<{float}> : std::iter::Iterator`
          `&mut std::ops::Range<{float}> : std::iter::Iterator`

How can I implement this loop in Rust?

Shepmaster
  • 326,504
  • 69
  • 892
  • 1,159
serge1peshcoff
  • 3,858
  • 10
  • 39
  • 68

4 Answers4

17

If you haven't yet, I invite you to read Goldberg's What Every Computer Scientist Should Know About Floating-Point Arithmetic.

The problem with floating points is that your code may be doing 200 or 201 iterations, depending on whether the last step of the loop ends up being i = 0.99 or i = 0.999999 (which is still < 1 even if really close).

To avoid this footgun, Rust does not allow iterating over a range of f32 or f64. Instead, it forces you to use integral steps:

for i in -100i8..100 {
    let i = f32::from(i) * 0.01;
    // ...
}

See also:

Shepmaster
  • 326,504
  • 69
  • 892
  • 1,159
Matthieu M.
  • 268,556
  • 42
  • 412
  • 681
  • 4
    I don't think this is a foot gun. Fp math is still deterministic, we could determine the number of iterations that it is going to be at compile time (always 200 or 201, the only reason I don't know for sure is I haven't bothered to do the math). In most contexts with fp you don't need an exact number of iterations, you are just trying to make it so there are regularly spaced ticks on your graph or something. The only problem I can see is if you size an array with the assumption that a certain number of elements would fit, but then you are going to need integers to index that array anyway. – Joseph Garvin Aug 02 '20 at 15:40
  • @JosephGarvin: I agree that the number of iterations is deterministic, the problem is that it may not be _obvious_. It doesn't matter if the _compiler_ can deduce it, if the (human) reader cannot... or is not confident. – Matthieu M. Aug 02 '20 at 16:18
  • I guess that's fair and I guess there is always the risk that someone will try to index with floor(). – Joseph Garvin Aug 02 '20 at 23:24
  • @JosephGarvin It could affect whether or not you include a tick at the end – Solomon Ucko Aug 30 '21 at 14:38
5

As a real iterator:

Playground

/// produces: [ linear_interpol(start, end, i/steps) | i <- 0..steps ]
/// (does NOT include "end")
///
/// linear_interpol(a, b, p) = (1 - p) * a + p * b
pub struct FloatIterator {
    current: u64,
    current_back: u64,
    steps: u64,
    start: f64,
    end: f64,
}

impl FloatIterator {
    pub fn new(start: f64, end: f64, steps: u64) -> Self {
        FloatIterator {
            current: 0,
            current_back: steps,
            steps: steps,
            start: start,
            end: end,
        }
    }

    /// calculates number of steps from (end - start) / step
    pub fn new_with_step(start: f64, end: f64, step: f64) -> Self {
        let steps = ((end - start) / step).abs().round() as u64;
        Self::new(start, end, steps)
    }

    pub fn length(&self) -> u64 {
        self.current_back - self.current
    }

    fn at(&self, pos: u64) -> f64 {
        let f_pos = pos as f64 / self.steps as f64;
        (1. - f_pos) * self.start + f_pos * self.end
    }

    /// panics (in debug) when len doesn't fit in usize
    fn usize_len(&self) -> usize {
        let l = self.length();
        debug_assert!(l <= ::std::usize::MAX as u64);
        l as usize
    }
}

impl Iterator for FloatIterator {
    type Item = f64;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current >= self.current_back {
            return None;
        }
        let result = self.at(self.current);
        self.current += 1;
        Some(result)
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let l = self.usize_len();
        (l, Some(l))
    }

    fn count(self) -> usize {
        self.usize_len()
    }
}

impl DoubleEndedIterator for FloatIterator {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.current >= self.current_back {
            return None;
        }
        self.current_back -= 1;
        let result = self.at(self.current_back);
        Some(result)
    }
}

impl ExactSizeIterator for FloatIterator {
    fn len(&self) -> usize {
        self.usize_len()
    }

    //fn is_empty(&self) -> bool {
    //    self.length() == 0u64
    //}
}

pub fn main() {
    println!(
        "count: {}",
        FloatIterator::new_with_step(-1.0, 1.0, 0.01).count()
    );
    for f in FloatIterator::new_with_step(-1.0, 1.0, 0.01) {
        println!("{}", f);
    }
}
Stefan
  • 4,914
  • 1
  • 21
  • 40
3

Another answer using iterators but in a slightly different way playground

extern crate num;
use num::{Float, FromPrimitive};

fn linspace<T>(start: T, stop: T, nstep: u32) -> Vec<T>
where
    T: Float + FromPrimitive,
{
    let delta: T = (stop - start) / T::from_u32(nstep - 1).expect("out of range");
    return (0..(nstep))
        .map(|i| start + T::from_u32(i).expect("out of range") * delta)
        .collect();
}

fn main() {
    for f in linspace(-1f32, 1f32, 3) {
        println!("{}", f);
    }
}

Under nightly you can use the conservative impl trait feature to avoid the Vec allocation playground

#![feature(conservative_impl_trait)]

extern crate num;
use num::{Float, FromPrimitive};

fn linspace<T>(start: T, stop: T, nstep: u32) -> impl Iterator<Item = T>
where
    T: Float + FromPrimitive,
{
    let delta: T = (stop - start) / T::from_u32(nstep - 1).expect("out of range");
    return (0..(nstep))
        .map(move |i| start + T::from_u32(i).expect("out of range") * delta);
}

fn main() {
    for f in linspace(-1f32, 1f32, 3) {
        println!("{}", f);
    }
}
user25064
  • 1,730
  • 2
  • 14
  • 27
  • 1
    Buffering in a `Vec` seems like a really bad idea to me. You could at least go for the nightly feature `conservative_impl_trait` and return `impl Iterator` if you don't want to implement an actual iterator. – Stefan Dec 19 '17 at 00:55
  • I will add the nightly version. I was trying to get it to work with the actual return type on stable but couldn't figure out how to get it to work with the closure that is capturing variables around it. – user25064 Dec 19 '17 at 12:49
  • As far as I know you can't unless you box the closure (or the iterator). – Stefan Dec 19 '17 at 12:52
1

This is basically doing the same as in the accepted answer, but you might prefer to write something like:

for i in (-100..100).map(|x| x as f64 * 0.01) {
    println!("Index {}", i);
}
Shepmaster
  • 326,504
  • 69
  • 892
  • 1,159
d2weber
  • 53
  • 4