Reputation: 4650
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?
Upvotes: 20
Views: 18737
Reputation: 6328
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).
Upvotes: 1
Reputation: 300049
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:
Upvotes: 32
Reputation: 363
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);
}
Upvotes: 12
Reputation: 5530
As a real iterator:
/// 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);
}
}
Upvotes: 6
Reputation: 2120
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);
}
}
Upvotes: 4