Electric Field Simulator and Art Creator

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()
}
1 Like