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