Skip to content

Instantly share code, notes, and snippets.

@Aatch
Forked from kolmodin/rust-json.rs
Last active July 5, 2023 04:22
Show Gist options
  • Save Aatch/5734372 to your computer and use it in GitHub Desktop.
Save Aatch/5734372 to your computer and use it in GitHub Desktop.
An example and explanation of how to use lifetimes and borrowing to avoid copying, while maintaining safety.
extern mod extra;
use extra::json::*;
/*
* This function manages to do absolutely no copying, which is pretty cool.
*
* "What are all those `'r`s?" you ask. Well, they're liftime parameters. They
* indicate how long something lasts (before it's freed). They can't change how
* long something lives for, they only allow you to tell the compiler stuff it
* would otherwise not be able to figure out.
*
* In this case, we have `'r`, but the name doesn't matter, just the initial
* single quote that makes it different to a type parameter. We use this lifetime
* in two places: the type signature of `json`, `&'r Json` which should be read
* as "A borrowed pointer to a Json type, that lasts for at least `'r`". It is also
* in the return type, `Option<&'r str>`. Ignoring the wrapping Option type, this
* should be read as "a slice into a string that lasts for at least `'r`".
*
* More comments inline!
*/
fn get_string<'r>(json: &'r Json, key: &str) -> Option<&'r str> {
// Dereferencing the json variable here doesn't matter, it just allows us
// to omit the leading `&` in the match arms.
match *json {
// This is a matching pattern against the `Object` variant of the `Json`
// enum. Since this variant contains a map, and that's what we want, we
// bind the variable `map` to it. The `ref` in there says that we want
// to bind to `map` by _reference_.
Object(ref map) => {
// Here, we use `find_equiv`, instead of `find`. `find_equiv` works
// for types that are **equivalent** to the key of the map.
// We use this method because otherwise we would need to use a ~str,
// which would be moved into this function, forcing the caller to
// either copy the string if they wanted to keep using it, or allocate
// unnecessarily if they wanted to use a static string (like a string
// literal).
//
// NB: Currently a limitation means we have to pass the slice by
// reference.
match map.find_equiv(&key) {
// Here we match against the `Some` variant of `Option`, and
// the `String` variant of `Json`. The `&` in there isn't borrowing,
// instead it's matching a borrowed value.
// Then, inside the the match, we bind the actual `~str` by reference
// to `s`. We can't bind it directly because that would mean we would
// move it out of the match, which we aren't allowed to do.
Some(&String(ref s)) => {
// Finally, construct a new `Some`, and turn it into a slice. The
// slice lasts as long as the string being sliced.
// This returns and the caller knows that the enclosed string (if
// there is one) lasts for at least as long as the json object
// it came from.
//
// (We could not slice and return a `&'r ~str` here, but that would
// behave oddly and be hard to use in other places, so this is much
// easier)
Some(s.as_slice())
}
_ => None
}
},
_ => None
}
}
/*
* This works because the lifetime given in the function signature is threaded down
* through the code. Each ref and match knows the lifetime and passes it on. Furthermore,
* the method calls know about lifetimes, so `find_equiv` gives you a pointer that lasts
* for as long as the map it came from and `as_slice` gives you a slice that lasts for as
* long as the string it came from. The compiler matches up all these lifetimes, and checks
* to see if what you say is true.
*
* The advantage of this version is that it moves the choice of memory usage to the caller.
* If they want to copy the string, then they can, but what if they just want to check to see
* if the key is in the object? Previously that would have incurred an allocation of the string,
* which could be huge. Much better idea to instead give them a slice and let them copy it if
* they want, instead of forcing it on them.
*/
fn main() {
// Use the magic stringify! macro because json tokenizes!
let json = from_str(stringify!(
{
"language": "Rust",
"level" : 9001
}
)).unwrap();
println(fmt!("%?", get_string(&json, "language"))); // Prints Some("Rust")
println(fmt!("%?", get_string(&json, "level"))); // Prints None
}
@flaper87
Copy link

Hey, this is a great example.

I've a rust-for-real repo with some examples / tutorials about rust. Would you like to propose a PR with this great example?

TBH, the rust-for-real is moving slow, but I plan to keep adding things

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment