Rust #5: Naming conventions

cthutu

Matt Davies

Posted on July 17, 2021

Rust #5: Naming conventions

This week, I wanted to clarify in my head what the naming conventions are in the standard library if any, and to see if they help with communicating concepts in the API.

A good API has well-established concepts and paradigms and a good language to communicate that. When you see a certain proposition in a name or verb, you understand what that API is trying to do. Good examples are MacOSX's Cocoa API. Bad examples are the Windows Win32 API. For me, the most important thing is consistency.

I feel that Rust's std library API is trying to achieve the same thing, and I want to explore what that language is.

I have already seen some of this with the prefixes and suffixes of some methods of Option and Result, such as or and or_else and more simply the use of is.

Let us go through the tools that the std library uses to convey information to us.

Casing

The Rust compiler is very opinionated about what casing and style you use to name things, even giving warnings when you break its rules. You can disable those warnings if you wish. Initially, I HATED the casing rules as I was used to my styles with my C++ background, and I have to admit, was a contention point for me in using Rust. But after a week of writing Rust code, I soon got used to it and appreciate there is consistency if not exactly what I'd prefer.

Here is a quick overview of the rules:

Convention Types that use it
snake_case Crates, modules, functions, methods, local variables and parameters, lifetimes.
CamelCase Types (including traits and enums), type parameters in generics.
SCREAMING_SNAKE_CASE Constant and static variables.

This means that when you have a type name and you want to refer to it in a function name, you have to convert. So YourType will become your_type. For example:

struct BankAccount {
    id: u64,
    name: String,
    amount: u128,
}

fn find_bank_account(id: u64) -> Rc<BankAccount> { ... }
Enter fullscreen mode Exit fullscreen mode

Ok, this is general Rust stuff and not specific to the std library, but I thought it was worth going over.

Basic types

The standard library naming convention documentation lists various pieces of text that are in method names based on the types that they operate on:

Type name Text in methods
&T ref
&mut T mut
&[T] slice
&mut [T] mut_slice
&[u8] bytes
&str str
*const T ptr
*mut T mut_ptr

Get and Set methods

Methods that fetch or set a value within their struct are usually called Getter and Setter methods.

For Setter methods, the prefix set_ is used with the field name. For Getter methods, no prefix is used and just the field's name is. For example:

struct Person {
    name: String,
    age: u16,
}

impl Person {
    fn new(name: String, age: u16) -> Self {
        Person { name, age }
    }

    // Getters

    fn name(&self) -> &str { &self.name }
    fn age(&self) -> u16 { self.age }

    // Setters

    fn set_name(&mut self, name: &str) { self.name = name.to_string(); }
    fn set_age(&mut self, age: u16) { self.age = age; }
}
Enter fullscreen mode Exit fullscreen mode

Suffixes

Suffixes are pieces of text that appear at the end of a name. And the std library has a few. Below is a list I've discovered.

Suffix Meaning Example
err Deals with the error part of a result Result::map_err()
in This function uses a given allocator Vec::new_in
move Owned value version for a method that normally returns a referenced value
mut Mutable borrow version of method that normally returns owned or referenced value &str::split_at_mut()
ref Reference version for a method that normally returns an owned value Chunks::from_ref()
unchecked This function is unsafe - there may be dragons beyond! &str::get_unchecked()

Due to the nature of Rust, many methods have variants. If a method foo returns an immutably borrowed value, then there are sometimes variants that return a mutably borrowed or an ownable copy. The suffixes mut, move and ref are used for this. For example:

fn foo() -> &T;             // original method
fn foo_mut() -> &mut T;     // mutable version
fn foo_move() -> T;         // owned version

fn bar() -> T;              // original version
fn bar_ref() -> &T;         // immutably borrowed version
fn bar_mut() -> &mut T;     // mutably borrowed version
Enter fullscreen mode Exit fullscreen mode

However, there are exceptions. For conversion to an iterator returning owned values, into_iter() is used instead of iter_move(). Also, if the method contains a type name and it returns a mutable borrow, the mut appears before the type. For example:

fn as_bar(foo: &Foo) -> &Bar;
fn as_mut_bar(foo: &Foo) -> &mut Bar;
Enter fullscreen mode Exit fullscreen mode

err is used with Results for methods that work with the error part instead of the OK part.

unchecked states that this method or function is unsafe. Make sure you add your protections around it. This suffix should be a BIG red flag to any user wanting to stay within Rust's safety. Usually, the documentation contains information on what you should do around this function to make sure it's still safe. For example, on &str::get_unchecked(i) the documentation asks that you:

  • The starting index must not exceed the ending index.
  • Indices must be within bounds of the original slice.
  • Indices must lie on UTF-8 sequence boundaries.

Now, it may be that given index i, your program has already ensured that these conditions are true; in which case you can go ahead and use get_unchecked instead of get because it will be more efficient. However, as a programmer, whenever you see the unchecked suffix, you should be reaching for the documentation to see what those conditions are.

in is not stable yet and Standard Library methods that use this suffix are only in the nightly compiler as of the time of writing. This indicates a variant that allows the user to pass in their allocator for allocation. For example, whereas Vec::new creates a vector that will use the default allocator when adding elements, Vec::new_in requires an allocator object that implements the Allocator trait that will be responsible for allocations.

Occasionally, there are times where you want to have multiple suffixes. If you do and one of them is mut, the convention is that mut should be last.

Prefixes

Prefix Meaning Example
as Free conversion from original &[u8]::as_bytes()
into Conversion that consumes original value String::into_bytes()
is Query method that returns a bool Vec::is_empty()
new Indicates a constructor Box::new_zeroed()
to Expensive conversion from original &str::to_owned()
try This variant returns a Result instead panicking Box::try_new()
with The variant uses a function to provide a value instead of directly RefCell::replace_with()

as, to and into prefixes indicate conversions. But there are different types of conversions. There are conversions that are free because they are basically no-ops. There are conversions that are expensive because a new type is constructed from the old. And there are conversions that consume the original value to produce the new one. Rust differentiates between these conversions by using prefixes.

as essentially exposes a view to an underlying representation that is present in the original value. For example, a slice from a vector. as methods do not affect the original value and usually produce a borrowed reference. This means that the conversion's lifetime must be shorter than the original value.

to is an expensive conversion that constructs a new value from the old one. For example, creating a new String from a string slice. These methods usually produce an ownable value.

into can be expensive and can be a no-op depending on what it does. However, it consumes the original value. An example of this is converting a String to a Vec<u8> using into_bytes(). The String contains a Vec<u8> under the bonnet, and so producing it is effectively a no-op. But because it's given away the underlying structure, the String cannot exist anymore.

Another example is into_iter(), which consumes the original container and produces an iterator that can give back its values. It can do this because the new iterator value owns the values and not the original container.

These prefixes are essential in understanding the semantics of the operations their methods provide.

new is often the complete name of a static method for constructing an instance of a struct. However, sometimes there are different ways to do construction and therefore multiple variants of new. These all have the new prefix.

try, often used with new for the try_new prefix indicates a version of a method that can fail gracefully. For example:

// This function will panic if the id does not exist.
fn get_config(id: u32) -> Config { ... }

// This will not panic and will return a Result instead.
fn try_get_config(id: u3) -> Result<Config, ConfigError> { ...  }
Enter fullscreen mode Exit fullscreen mode

If the Result version is the only version, then there is no need for the prefix try. It's only used for variants.

with indicates a method that whereas the original function passed a value as a parameter, this variant will be passed a function that provides that value instead. To be fair, I have not seen this suffix used that often. I have even seen other suffixes used to mean the same thing. For example, should Option::ok_or_else really be Option::ok_or_with?

Conclusion

I found this research to be enlightening and helpful in understanding the Standard Library better.

Please comment below about any other prefixes or suffixes that I might have missed - I am sure that there are many.

Also, if you've been enjoying my series of random articles, please let me know what you want me to look into next.

Until next week!

💖 💪 🙅 🚩
cthutu
Matt Davies

Posted on July 17, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Daemons on macOS with Rust
undefined Daemons on macOS with Rust

November 29, 2024

Baby Steps with Rust
programming Baby Steps with Rust

November 29, 2024