16

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
  • 388,571
  • 95
  • 1,107
  • 1,366
serge1peshcoff
  • 4,342
  • 11
  • 45
  • 76

5 Answers5

26

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
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • 9
    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
  • 4
    Honestly the convenience of being able to specify floating point steps and bounds far out weights the "problem" of doing one more or one less iteration. I do lots of geometric computations, like searching a scalar function along a line, creating points along a parametric curve, discretizing a contiguous function along a domain... In all of these cases i don;t care about the number of iterations, I care about the step size and the bounds, having one more or one less sample due to a floating point imprecission is a non issue, but converting from integers to floats and back is annoying. – Makogan Aug 15 '22 at 03:56
6

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
  • 5,304
  • 2
  • 25
  • 44
6

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
  • 388,571
  • 95
  • 1,107
  • 1,366
d2weber
  • 163
  • 1
  • 5
4

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
  • 2,020
  • 2
  • 16
  • 28
  • 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

For the reasons mentioned by others, one shouldn't be looping using floats under most circumstances.

For those cases where it is appropriate, it can be done (although not as ergonomically, which is probably good design--Rust should make it more difficult to juggle running chainsaws).

Since Rust 1.34, std::iter::successors() enables looping directly with a floating point index:

use std::iter;

const START: f64 = -1.0;
const END: f64 = 1.0;
// Increment by 0.1 (instead of 0.01 per the question) for output brevity
const INCREMENT: f64 = 0.1;

fn main() {
    iter::successors(Some(START), |i| {
        let next = i + INCREMENT;
        (next < END).then_some(next)
    })
    .for_each(|i| println!("{i}"));
}

Note there are 21 lines of output, although only 20 were probably expected given the condition of i < 1.0 (as opposed to i <= 1.0) in the sample code of your question.

This is due to the precision and/or cumulative rounding errors present in the output, even though the source code specifies iterating from -1.0 to 1.0 in increments of exactly 0.1. (Feel free to switch the START value to 0.0 or 0.3 to see different series output, also with precision/cumulative rounding errors).

Playground example

U007D
  • 5,772
  • 2
  • 38
  • 44