Let’s make things better today.
We’re humans. We’re horrible at repetition. We like entertainment. For repetitive tasks, we have tools like loops that we can utilise.
An example from BrewStillery looked like this originally:
pub fn increaseABVMaths(allInputs: &increaseABVData) -> finalSugarFloat {
let mut newStartingBrix: f64 = allInputs.startingBrix;
let mut newEstimatedABV: f64 = realABVAndAttenuation(newStartingBrix, FINAL_BRIX_IDEAL).0;
while newEstimatedABV < allInputs.desiredABV {
newStartingBrix = newStartingBrix + 0.001;
newEstimatedABV = realABVAndAttenuation(newStartingBrix, FINAL_BRIX_IDEAL).0;
}
...
What’s going on is newStartingBrix
is a user inputted f64
.
newEstimatedABV
is set to be the ABV return value of the function
realABVAndAttenuation()
.
Then, we say while
newEstimatedABV
is less than the user’s desired
ABV, increment newStartingBrix
by 0.001
, use that value as an input
in realABVAndAttenuation()
, and store the resultant ABV in newEstimatedABV
.
To our perception, this works basically instantly on modern hardware.
We can still improve things though. Rust has a fantastic crate called
Rayon, which allows us to parallelise
Rust’s .iter()
method. We do that by changing it from .iter()
to
.par_iter()
. It’s as straightforward as it can be.
The issue is that nowhere in the Rust documentation does it say anything
about parallelising a while loop. There are great examples of for
loops,
but that doesn’t help us here.
What to do?
Well, we have to work with some goofy bounds. In Rust, iterators are
only allowed to use integers. So we can’t create one like 0.0..33.0
.
In this particular example, I know the range that brix can be.
It is a sugar density which is measured by a refractometer. It’s maximum
value is 32
.
If we employ a bit of mathy inversion trickery, we can come up with something
like this: (newStartingBrix * 1000.0) as u32)..33000)
.
All we’re doing here is getting rid of the decimal (f64
) and turning
it into a large integer (u32
).
Then we convert it back to a float inside the .map()
by doing this:
let tempBrix: f64 = ( mapBrix as f64 / 1000.0 ) + 0.0001;
I changed the range from 32
to 33
because I wanted a little more
buffer on the top end, and I love 3’s.
Something to note, is that if our iterator goes past 33000
, this will
crash our program. There are input guards in a preceeding function
that rejects any input greater than 32
.
The final code looks like this:
pub fn increaseABVMaths(allInputs: &increaseABVData) -> finalSugarFloat {
let mut newStartingBrix: f64 = allInputs.startingBrix;
newStartingBrix = (((newStartingBrix * 1000.0) as u32)..33000)
.into_par_iter()
.map(|mapBrix| {
let tempBrix: f64 = ( mapBrix as f64 / 1000.0 ) + 0.0001;
let tempABV: f64 = realABVAndAttenuation(tempBrix,FINAL_BRIX_IDEAL).0;
(tempBrix, tempABV)
})
.find_first(|(_tempBrix, tempABV)| {
allInputs.desiredABV < *tempABV
})
.expect("increaseABVPrep(), newEstimatedABV")
.0;
...
To run through two for
loop examples, we can parallelise our twoArraySum()
:
pub fn twoArraySum(firstArray: [f64; 81], secondArray: [f64; 81]) -> f64 {
let mut sum: f64 = 0.0;
// this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
for index in 0..81 {
sum = sum + firstArray[index] * secondArray[index];
}
sum
}
Which becomes:
pub fn twoArraySum(firstArray: [f64; 81], secondArray: [f64; 81]) -> f64 {
// this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
firstArray
.par_iter()
.zip(secondArray.par_iter())
.map(|(first, second)|{
first * second
}).sum()
}
If you’re not familiar with it, the .zip()
method pairs up iterators.
We then throw that into .map()
and just replicate the maths from the
original for
loop.
And finally, our threeArraySum()
:
pub fn threeArraySum(firstArray: [f64; 81], secondArray: [f64; 81], thirdArray: [f64; 81]) -> f64 {
let mut sum: f64 = 0.0;
// this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
for index in 0..81 {
sum = sum + firstArray[index] * secondArray[index] * thirdArray[index];
}
sum
Becomes:
pub fn threeArraySum(firstArray: [f64; 81], secondArray: [f64; 81], thirdArray: [f64; 81]) -> f64 {
// this does the weird spreadsheet thing of (array1[0] * array2[0]) + (array1[0] * array2[0]) ...
firstArray
.par_iter()
.zip(secondArray.par_iter())
.zip(thirdArray.par_iter())
.map(|((first, second), third)|{
first * second * third
}).sum()
The unique thing here is what’s going in .map()
.
The syntax for mapping multiple .zip()
’s is a bit weird.
It only accepts a tuple, so we have to group them in nested pairs.
If we were going to do a fourth iterator, it would look like this:
(((first, second), third), fourth)
for the .map()
arguments.
Now, we’ve safely