How to do a 'for' loop with boundary value

2019-07-07 19:26发布

问题:

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?

回答1:

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 -100..100 {
    let i = i as f32 * 0.01;
    // ...
}


回答2:

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);
    }
}


回答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);
    }
}


标签: for-loop rust