How to rewrite Elm code in Rust for performance?
For those curious about the difference between Rust and Elm and how hard it is to rewrite performance-critical parts in Rust, my experience is that translation is actually very straight-forward and definitoin-by-definition. Like Elm, Rust is expression-oriented, uses combinators like fold and map, uses sum and product types, etc. The hardest part is to find similar libraries in Rust but everything afterward is much easier.
Here’s a comparison of the Field
type in Elm and Rust:
Elm
type alias Field =
{ source: Charge
, density: Int
, steps: Int
, delta: Float
, lines: List Line
}
Rust
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Field {
source: Charge,
density: usize,
steps: usize,
delta: f64,
#[serde(skip_deserializing)]
lines: Vec<Line>,
}
Here’s a comparison of the core calculation function in Elm and Rust:
Elm (73 lines)
calculateFieldLine :
{ charges : List Charge
, steps : Int
, delta : Float
, sourceSign : Sign
, start : Point
, xBound : Float
, yBound : Float
} -> Line
calculateFieldLine { charges, steps, delta, sourceSign, start, xBound, yBound } =
foldlWhile
(\_ line ->
let
(x, y) =
case line of
prev :: _ ->
prev
_ ->
(0, 0) -- impossible
previousPosition =
vec2 x y
outOfBounds =
x > xBound || x < 0 || y > yBound || y < 0
netField =
if outOfBounds then
Vector2.vec2 0 0
else
List.foldl
(\charge sum ->
let
d =
Vector2.distance previousPosition charge.position / 100
magnitude =
charge.magnitude / (d ^ 2)
sign =
case charge.sign of
Positive ->
1
Negative ->
-1
field =
Vector2.scale (sign * magnitude) <|
Vector2.normalize <|
Vector2.sub previousPosition charge.position
in
Vector2.add sum field
)
(Vector2.vec2 0 0)
charges
next =
if outOfBounds then
(x, y)
else
let
vec =
Vector2.add
(Vector2.vec2 x y)
((case sourceSign of
Positive ->
identity
Negative ->
Vector2.negate
)<|
Vector2.scale delta <|
Vector2.normalize netField
)
in
(Vector2.getX vec, Vector2.getY vec)
in
(next :: line, outOfBounds)
)
[ start ]
(List.range 0 (steps - 1))
Rust (51 lines)
fn calculate_field_line(charges: &Vec<Charge>, steps: usize, delta: f64, source_sign: Sign, start: Point, x_bound: f64, y_bound: f64) -> Line {
(0..steps - 1).fold_while(vec![ start ], |mut line: Line, _| {
let [x, y] = match line {
_ if line.len() > 0 =>
line[line.len()-1],
_ =>
[0.0, 0.0] // impossible
};
let previous_position: Vector2 = [x, y];
let tolerance = 100.0;
let out_of_bounds = x > x_bound + tolerance || x < -tolerance || y > y_bound + tolerance || y < -tolerance;
if out_of_bounds {
Done(line)
} else {
let net_field =
charges.iter().fold([0.0, 0.0], |sum, charge| {
let charge_position = position_to_vector2(&charge.position);
let d = distance(previous_position, charge_position) / 100.0;
let magnitude = charge.magnitude / d.powf(2.0);
let sign =
match charge.sign {
Sign::Positive => 1.0,
Sign::Negative => -1.0
};
let field =
vecmath::vec2_scale(
vecmath::vec2_normalized(
vecmath::vec2_sub(previous_position, charge_position)
), sign * magnitude);
vecmath::vec2_add(sum, field)
});
let delta_vector =
vecmath::vec2_scale(
vecmath::vec2_normalized(net_field),
delta
);
let next =
vecmath::vec2_add(
previous_position,
match source_sign {
Sign::Positive =>
delta_vector,
Sign::Negative =>
vecmath::vec2_neg(delta_vector),
}
);
line.push(next);
Continue(line)
}
}).into_inner()
}