Lesson 2
Meaningful Naming in Rust
Introduction

Welcome to the second lesson of the "Clean Code Basics with Rust" course, focusing on meaningful naming. In the previous lesson, we introduced clean code and its significance in developing maintainable and efficient software. Now, let's explore the importance of meaningful naming — an essential part of clean code. Selecting appropriate names is vital for creating code that is clear, understandable, and easy to maintain.

Good Naming at a Glance

In this lesson, we'll cover the following naming guidelines:

  • Reveal Intent Through Names: Ensure names clearly convey the role and functionality of variables, structures, and functions. For instance, replacing calc with calculate_interest enhances code clarity. 🧠

  • Avoid Misleading Names: Avoid names that imply incorrect assumptions, such as using users_list for a HashSet, ensuring accuracy and understanding. 🚫

  • Choose Descriptive, Searchable Names: Opt for names like age instead of a, facilitating easy searchability and recognition within the codebase, which enhances maintainability. 🔍

  • Name Traits and Implementations Wisely: Use clear and descriptive names that reflect behavior or capabilities, such as Readable for traits and FileReader for implementations, without unnecessary prefixes or suffixes.

  • Consistent Naming Across the Codebase: Use uniform patterns like get_all_users instead of varied terms such as fetch_all_users, maintaining clarity and preventing confusion. 📚

  • Provide Sufficient Context in Names: Include enough context, such as using file_size instead of size, to eliminate ambiguity, especially when components are used across different contexts. 🌐

Reveal Intent Through Names

Names should clearly express the purpose and functionality of your variables, structures, and functions, leaving no room for ambiguity.

Bad ExampleGood Example
struct MyStruct { id: u32, name: String }struct User { id: u32, name: String }
let coll: Vec<User>;let users: Vec<User>;
fn calc() -> f64 { /* ... */ }fn calculate_interest() -> f64 { /* ... */ }
let temp: f64;let temperature: f64;
fn proc() -> Result<(), Error> { /* ... */ }fn process_order() -> Result<(), Error> { /* ... */ }

Effective names provide immediate insight into what the code does, reducing the need for additional explanations. For example, replacing let coll: Vec<User>; with let users: Vec<User>; instantly conveys the collection's purpose and the type of data it holds.

Avoid Misleading Names

Avoid using names that may lead others to incorrect assumptions about the type or purpose of a variable or function.

Bad ExampleGood ExampleExplanation
let users_list: HashSet<User>;let users: HashSet<User>;The name suggests a list, but it's actually a set.
fn save_user(user: &User) { /* ... */ }fn save_user_and_send_confirmation(user: &User) { /* ... */ }The function name doesn't convey that it also sends a confirmation.
let temp: f64;let temperature: f64;The name "temp" could be misinterpreted as "temporary."

Ensure that variable and function names accurately represent their true purpose and type, preventing misunderstandings and errors.

Choose Descriptive, Searchable Names

Names should be easily searchable within the codebase. Using short names, even if they seem descriptive in certain contexts, generally hinders maintainability and readability. For example, opting for number_of_items instead of num makes the code easier to search and understand.

Avoid unnecessary abbreviations unless they are widely recognized in the Rust community (e.g., cfg, fmt): spelling out words fully ensures code is accessible to all readers.

Name Traits and Implementations Wisely

In Rust, traits are often named to reflect capabilities or behaviors, using nouns or adjectives. Avoid using legacy conventions or terminology from other languages like Service or appending Impl for implementations. Instead, choose names that clearly convey the trait's purpose and the implementation's specifics without redundancy.

For example, instead of naming a trait UserService and an implementation InMemoryUserService, consider naming them respectively UserRepository and InMemoryUserRepository.

Alternatively, using behavior-focused trait names, such as Authenticatable for the trait and names like PasswordAuthenticator and OAuthAuthenticator for implementations.

Consistent Naming Across the Codebase

Consider function names such as fetch_all_users, retrieve_tasks, load_users, and fetch_every_todo_item. Is anything wrong with these names? They do convey intent and are descriptive, so they appear fine. However, using these varied names within the same codebase is problematic due to inconsistency.

In the same codebase, it's beneficial to stick to a single naming pattern that aligns with Rust's standard library conventions, like iter, get, or new, to avoid confusion and maintain clarity. For example:

  • fn get_all_users() -> Vec<User> { /* ... */ }
  • fn get_all_tasks() -> Vec<Task> { /* ... */ }
  • fn get_all_todo_items() -> Vec<TodoItem> { /* ... */ }

Consistency and familiarity with standard patterns enhance code readability and maintainability.

Provide Sufficient Context in Names

When discussing good naming, consider the context in which a name is used. The variable name size might be perfectly acceptable within the resize_array function. However, in the context of generate_report, the name is too vague, and renaming this variable to something more descriptive like number_of_pages is advisable.

Rust's module system provides context that can influence naming decisions. Since modules encapsulate functionality, names within a module can be shorter without losing clarity. For example, within a user module, a function named fn save(user: &User) { /* ... */ } carries sufficient context, and there's no need for an overly verbose name like save_user.

Balancing name length with clarity is essential. Leverage the module system to provide context, reducing the need for excessively long names while avoiding ambiguity.

Rust Naming Conventions at a Glance

In Rust, following standard naming conventions enhances code readability and consistency. Rust uses snake_case for variable and function names, and CamelCase for structs, enums, and traits. Adhering to these conventions and choosing meaningful names improves the clarity of your code.

ItemNaming ConventionExample
Variables and Functionssnake_casecalculate_interest
Structs and EnumsCamelCaseUser, OrderItem
TraitsCamelCase, often nouns or adjectivesReadable, Serializable
ConstantsSCREAMING_SNAKE_CASEMAX_ITEMS, DEFAULT_RATE

For more details, refer to the Rust API Guidelines and the Rust Style Guidelines.

Summary

Meaningful naming is a critical aspect of writing clean code. By choosing names that clearly express intent, avoiding misleading terms, and maintaining consistency and context, you create code that is easy to read, understand, and maintain. Up next, you'll have the opportunity to refactor code, applying these principles and honing your ability to write intuitive, clean code.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.