ruk·si

🦀 Rust
🐮 Bovine Ergonomics

Updated at 2024-02-24 02:11

If you want to make more ergonomic programming interface, you can use cows.

Cow keeps the user guessing if the value is to be consumed. 😋🥩

use std::borrow::Cow;

fn do_it<'a>(text: impl Into<Cow<'a, str>>) {
    match text.into() {
        Cow::Borrowed(s) => println!("borrowed: {}", s),
        Cow::Owned(s) => println!("owned: {}", s),
    }
}

// if you return cows, Rust can more easily figure out the lifetime
fn lifetime_from_parameter(input: &str) -> Cow<str> {
    Cow::Borrowed(&input[0..1])
}

fn main() {
    let txt = "Hello, world!".to_string();

    // text: impl Into<Cow<'a, str>>
    do_it("Hello, world!");
    do_it(&txt);
    do_it(txt);
    let xxx = lifetime_from_parameter("lol");
    dbg!(xxx);

    // text: &str
    //do_it("Hello, world!");
    //do_it(&txt);
    //do_it(txt.as_ref());

    // text: impl Into<&'a str>
    //do_it("Hello, world!");

    // text: impl AsRef<str> + std::fmt::Display
    //do_it("Hello, world!");
    //do_it(&txt);
    //do_it(txt);
}

How you would use an API like this:

  • if you don't need the variable after the call, just pass the value
  • if you need the variable after the call, pass a reference

This can make the API nicer to use, especially if you are not returning Cows but just taking Into<Cow>s. Can lead to more noisy internals though.

But, it does make the API less prone to breaking changes, and more open for future optimization without user modifications.

The original use-case is optimization. ⚡️ For example, take in a string and a bovine string that might have matching content to replace or not. If there is a match, the bovine string is mutated and Cow::Owned returned but if not, just the Cow::Borrowed is returned without any extra allocation happening.

Copy-on-Write - Cow

So, usually, you would only use Cow<'_, T> to:

  • Avoid Copying: T is expensive to clone and want to avoid it.
  • Mutate Conditionally: receiver wants to mutate the T sometimes
  • Keep Consist: your API is using mainly Cows elsewhere

You get the majority of the other benefits also from impl AsRef<str>, which also guarantees zero-cost conversion and is conceptually simpler.

fn do_stuff(text: impl AsRef<str>) {
    let text = text.as_ref();
    todo!()
}