Electric Field Simulator and Art Creator

How to optimize Elm with Rust?

Say we are trying to optimize the calculateFields function which calculates the positions of all field lines of a charge.

In Elm you may have the function defined as:

calculateFields : Float -> Float -> List Field -> List Field
calculateFields width height fields =
   ...

We will go through the basic logic of Interoperation between Elm and Rust. The code I show is just for demonstration and is incomplete for the sake of simplicity.

First step, create encode and decoder for all non-primitive types in the function type declaration (parameter and return types).

This process is very mechanical, you just map each field of a record to their encoder/decoder.

  • Encoding Fields:
encodeFields : List Field -> Encode.Value
encodeFields fields =
  Encode.list encodeField fields

encodeField : Field -> Encode.Value
encodeField { source, density, steps, delta } =
  Encode.object
  [ ("source", encodeCharge source)
  , ("density", Encode.int density)
  , ("steps", Encode.int steps)
  , ("delta", Encode.float delta)
  ]
  • Decoding Fields:
decodeFields : Decoder (List Field)
decodeFields =
  Decode.list decodeField

decodeField : Decoder Field
decodeField =
  Field.require "source" decodeCharge <| \source ->
  Field.require "density" Decode.int <| \density ->
  Field.require "steps" Decode.int <| \steps ->
  Field.require "delta" Decode.float <| \delta ->
  Field.attempt "lines" (Decode.list (Decode.list decodePoint)) <| \lines ->

  Decode.succeed
    { source = source
    , density = density
    , steps = steps
    , delta = delta
    , lines = Maybe.withDefault [] lines
    }

Second step, replace concrete Elm implementation with a port.

port calculateFieldsPort : (Float, Float, Encode.Value) -> Cmd msg

Third step, listen to the port’s return value through a subscription.

port receiveFieldsPort : (Encode.Value -> msg) -> Sub msg 
subscriptions : Model -> Sub Msg
subscriptions _ =
    receiveFieldsPort ReceivedFields

Fourth step, subscribe to Elm port in your JS code.

Notice the call to a wasm.calculate_fields. That will be our Rust function.

app.ports.calculateFieldsPort.subscribe(function([width, height, fields_in_json]) {
  app.ports.receiveFieldsPort.send(wasm.calculate_fields(width, height, fields_in_json));
});

Fifth step, define equivalent parameter types in Rust.

What’s great about Rust is that a serialization/deserialization library called Serde provides a default implementation of encoder/decoder for any type. The default is good enough for our case.

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Field {
  source: Charge,
  density: usize,
  steps: usize,
  delta: f64,
  #[serde(skip_deserializing)]
  lines: Vec<Line>,
}

Sixth step, define equivalent function in Rust.

Notice the JsValue type? You can pass in any JS types including strings, objects, arrays, and so on to Rust and the package wasm-bindgen will handle any wrapping and conversion for you.

#[wasm_bindgen]
pub fn calculate_fields( width: f64, height: f64, fields_in_json: &JsValue ) -> JsValue

If you want to learn more about how to power Elm with Rust, check out my template for creating a progressive web app using Elm and Rust. You may also want to check out Rust’s wasm-bindgen guide and more specifically how to pass arbitrary data between JS and Rust

3 Likes