2

Context: I am learning Rust & WebAssembly and as a practice exercise I have a project that paints stuff in HTML Canvas from Rust code. I want to get the query string from the web request and from there the code can decide which drawing function to call.

I wrote this function to just return the query string with the leading ? removed:

fn decode_request(window: web_sys::Window) -> std::string::String {
    let document = window.document().expect("no global window exist");
    let location = document.location().expect("no location exists");
    let raw_search = location.search().expect("no search exists");
    let search_str = raw_search.trim_start_matches("?");
    format!("{}", search_str)
}

It does work, but it seems amazingly verbose given how much simpler it would be in some of the other languages I have used.

Is there an easier way to do this? Or is the verbosity just the price you pay for safety in Rust and I should just get used to it?

Edit per answer from @IInspectable: I tried the chaining approach and I get an error of:

temporary value dropped while borrowed

creates a temporary which is freed while still in use

note: consider using a `let` binding to create a longer lived value rustc(E0716)

It would be nice to understand that better; I am still getting the niceties of ownership through my head. Is now:

fn decode_request(window: Window) -> std::string::String {
    let location = window.location();
    let search_str = location.search().expect("no search exists");
    let search_str = search_str.trim_start_matches('?');
    search_str.to_owned()
}

which is certainly an improvement.

Matthew Nichols
  • 4,866
  • 4
  • 41
  • 48

1 Answers1

4

This question is really about API design rather than its effects on the implementation. The implementation turned out to be fairly verbose mostly due to the contract chosen: Either produce a value, or die. There's nothing inherently wrong with this contract. A client calling into this function will never observe invalid data, so this is perfectly safe.

This may not be the best option for library code, though. Library code usually lacks context, and cannot make a good call on whether any given error condition is fatal or not. That's a question client code is in a far better position to answer.

Before moving on to explore alternatives, let's rewrite the original code in a more compact fashion, by chaining the calls together, without explicitly assigning each result to a variable:

fn decode_request(window: web_sys::Window) -> std::string::String {
    window
        .location()
        .search().expect("no search exists")
        .trim_start_matches('?')
        .to_owned()
}

I'm not familiar with the web_sys crate, so there is a bit of guesswork involved. Namely, the assumption, that window.location() returns the same value as the document()'s location(). Apart from chaining calls, the code presented employs two more changes:

  • trim_start_matches() is passed a character literal in place of a string literal. This produces optimal code without relying on the compiler's optimizer to figure out, that a string of length 1 is attempting to search for a single character.
  • The return value is constructed by calling to_owned(). The format! macro adds overhead, and eventually calls to_string(). While that would exhibit the same behavior in this case, using the semantically more accurate to_owned() function helps you catch errors at compile time (e.g. if you accidentally returned 42.to_string()).

Alternatives

A more natural way to implement this function is to have it return either a value representing the query string, or no value at all. Rust provides the Option type to conveniently model this:

fn decode_request(window: web_sys::Window) -> Option<String> {
    match window
          .location()
          .search() {
        Ok(s) => Some(s.trim_start_matches('?').to_owned()),
        _     => None,
    }
}

This allows a client of the function to make decisions, depending on whether the function returns Some(s) or None. This maps all error conditions into a None value.

If it is desirable to convey the reason for failure back to the caller, the decode_request function can choose to return a Result value instead, e.g. Result<String, wasm_bindgen::JsValue>. In doing so, an implementation can take advantage of the ? operator, to propagate errors to the caller in a compact way:

fn decode_request(window: web_sys::Window) -> Result<String, wasm_bindgen::JsValue> {
    Ok(window
        .location()
        .search()?
        .trim_start_matches('?')
        .to_owned())
}
IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • Thanks, great feedback. Added error that comes up when I try a pure chaining approach. If you thoughts on that, lovely, but either way your answer is already very helpful. – Matthew Nichols May 04 '20 at 14:08
  • @mat I see [the error](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=386f964d12002c71d480eaafed7a2ed5) now. `search().unwrap()` returns a `String`, that's never assigned to a variable; it's lifetime ends at the end of the statement, yet `trim_start_matches` returns a slice reference, that outlives the temporary. I'll update the answer to fix this. – IInspectable May 04 '20 at 16:26
  • In the updated code, `trim_start_matches` still returns a slice reference into the temporary `String`. What's changed is that the slice reference itself is now a temporary. Its end of life coincides with that of its referent, the temporary `String` value. There's no longer an opportunity to create a dangling reference, and the borrow checker happy again. – IInspectable May 04 '20 at 16:43
  • Thanks. The difference in how Rust handles scope/lifetime of data from most languages is definitely the part I am still struggling with. – Matthew Nichols May 05 '20 at 08:35
  • 1
    @mat Lifetime is confined to the enclosing scope. That's a fairly traditional choice, shared with many programming languages. In fact, you'd run into the very same lifetime issues if [this were C++](https://wandbox.org/permlink/AeFPLB9k3eracSF3). What's unique about Rust is that it enforces lifetime *constraints* at compile time between references and referents. C++ doesn't, and the invalid code compiles just fine, with added malevolence of even producing the desired output. Taming the borrow checker is certainly an effort. I'd recommend *"Programming Rust"* if you don't have that already. – IInspectable May 05 '20 at 10:49