20
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.
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> { ... }
Ok, this is general Rust stuff and not specific to the std
library, but I thought it was worth going over.
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 |
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; }
}
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
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;
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.
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> { ... }
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
?
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!
20