It’s no secret that initializing constants at compile time is a good practice in Programming. Not only do you reduce the initialization overhead, but you also make it easier for the compiler to cleverly optimize your code by knowing the value of the constant in advance.
Sometimes, however, it’s impossible to initialize every constant at compile time since it requires performing non-constant operations or fetching data available only at runtime. For instance, say we make repetitive use of the number √7
in our program. Instead of calculating it every time, it would be better to define a constant for it like follows:
const ROOT_OF_SEVEN: f64 = 7_f64.sqrt();
This code, however, is invalid. The Rust compiler returns the following error:
cannot call non-const fn `f64::<impl f64>::sqrt` in constants
calls in constants are limited to constant functions, tuple structs and tuple variants
The same happens if we try to initialize a constant with an environment variable:
const LANG: String = env::var("LANG").unwrap();
From the Rust compiler:
cannot call non-const fn `std::env::var::<&str>` in constants
calls in constants are limited to constant functions, tuple structs and tuple variants
As you can see, certain constants that would be useful to initialize at compile time require non-constant operations. This is where Rust’s lazy_static
crate comes in handy. lazy_static
allows you to define global static variables that get initialized lazily, meaning that their values only get set upon their first actual usage at runtime. Lazy statics only need to be initialized the first time they’re used and, since this is a one-time operation, their runtime initialization overhead is negligible.
In this article, we’ll take a look at how to use Rust’s lazy_static
crate to lazily initialize global constants and a few of their use cases.
Use lazy_static
To use the lazy_static
crate, you simply add it to your project dependencies by
cargo add lazy_static
Once you added the crate, you can import the lazy_static!
macro in your source files like this:
use lazy_static::lazy_static;
To define lazy statics, enclose all the declarations within the lazy_static!
macro:
lazy_static! {
static ref LANG: String = env::var("LANG").unwrap();
static ref ROOT_OF_SEVEN: f64 = 7_f64.sqrt();
}
By doing this, you declare a global static reference to a lazily initialized value. To use these "lazy constants", you can just dereference them:
fn main() {
println!("System language: LANG is {}", *LANG);
println!("Square root of 7 is {}", *ROOT_OF_SEVEN);
}
Now you can effectively use the lazy_static
crate to initialize global constants at runtime. Keep reading to learn how it works.
What is lazy initialization?
When do lazy statics get initialized? In computing, the term lazy usually means that work is done exclusively if and when it’s required. Otherwise, lazy operations aren’t performed. In the case of lazy statics, they are initialized just before their first usage.
To make it clearer, take a look at the following code example:
lazy_static! {
static ref BYTES_WRITTEN: usize = {
let bytes_written = std::io::stdout().write(b"Initializing lazy_static!n").unwrap();
std::io::stdout().flush().unwrap();
bytes_written
};
static ref ANOTHER_LAZY_STATIC: String = {
println!("Initializing ANOTHER_LAZY_STATIC!");
"Hello, World!".to_string()
};
static ref UNUSED_LAZY_STATIC: String = {
println!("Initializing UNUSED_LAZY_STATIC!");
"Hello, World!".to_string()
};
}
All these lazy static initializations have side effects: they print stuff to the console. Using these side effects, we can detect when their values are actually initialized.
The main()
function:
fn main() {
println!("Doing stuff before using lazy statics...");
println!("Bytes written during initialization: BYTES_WRITTEN is {}", *BYTES_WRITTEN);
println!("Reusing an already initialized lazy static: BYTES_WRITTEN is {}", *BYTES_WRITTEN);
println!("ANOTHER_LAZY_STATIC is {}", *ANOTHER_LAZY_STATIC);
}
And the output order shows when each operation is performed at runtime:
Doing stuff before using lazy statics...
Initializing lazy_static!
Bytes written during initialization: BYTES_WRITTEN is 26
Reusing an already initialized lazy static: BYTES_WRITTEN is 26
Initializing ANOTHER_LAZY_STATIC!
ANOTHER_LAZY_STATIC is Hello, World!
As you can see from the first output line, no lazy static is initialized before it’s actually needed. The second and third output lines show that the initialization is performed exactly before the value is used.
The fourth line highlights the fact that no initialization is required for already used lazy statics since their value is stored.
Also, did you notice that UNUSED_LAZY_STATIC
was never initialized? That’s because lazy operations are performed only if they’re actually needed.
Further reading
This article was meant to be a basic tutorial on the lazy_static
crate and its most popular feature. For more information, read the official documentation.
I hope you enjoyed this article. If you have anything to add, please share your thoughts in a comment. Thanks for reading!
If you want to learn more about low-level programming, I highly recommend checking out this story below: