“Do the best you can until you know better. Then when you know better, do better.”
__ Maya Angelou
RUST : LET’S GET IT STARTED!
In the previous posts, we discussed more about the theory parts of Rust programming language.
▸ Installation & Hello World ▸ Cargo & Crates ▸ Variable bindings , Constants & Statics ▸ Comments ▸ Functions ▸ Primitive Data Types ▸ Operators
▸ Control Flows
▸ Vectors ▸ Structs ▸ Enums ▸ Generics ▸ Impls & Traits
▸ Ownership ▸ Borrowing ▸ Lifetimes & Lifetime Elision
But today we are going to discuss more about the the practical stuff of writing Rust programs.
Also now, you can read the same content via learning-rust.github.io 👈 . I think it’s more structured and easy to see the big picture.
Code Organization
When a single code block is getting larger, it should be decomposed into smaller pieces and should be organized in a proper manner. Rust supports different levels of code organization.
- Functions
- Modules
Can be mapped to a
▸ Inline module
▸ File
▸ Directory hierarchy - Crates
Can be mapped to a
▸ lib.rs file on the same executable crate
▸ Dependency crate specified on Cargo.toml
- Dependency can be specified from
▸ path
▸ git repository
▸ crates.io - Workspaces
Helps to manage multiple crates as a single project
Let’s discuss one by one.
💡 To make examples more simpler, we use a simple function which prints
“Hello, world!”
. But regarding writing testable codes, always try to return theString
from the function and print it when calling it, instead printing the String inside the function.
01. Functions
Functions are the first line of organization in any program.
We can add unit tests in the same file.
💭 An attribute is a general, free-form metadatum that is interpreted according to name, convention, and language and compiler version.
02. Modules
▸ Inline modules/ Modules in the same file
Related code and data are grouped into a module and stored in the same file.
Modules can also be nested.
Private functions can be called from the same module or from a child module.
💡
self
keyword is used to refer same module, whilesuper
keyword is used to refer parent module. Alsosuper
keyword can be used to access root functions from inside a module.
🔎 When writing tests it’s a good practice to write tests inside a test module because of they compile only when running tests.
▸ Modules - In a different file in the same directory
If we wrap file content with a mod
declaration, it will act as a nested module.
▸ Modules - In a different file in a different directorymod.rs
in the directory module root is the entry point to the directory module. All other files in that directory root, act as sub-modules of the directory module.
Again, If we wrap file content with a mod
declaration, it will act as a nested module.
Other files in the directory module act as sub-modules for mod.rs
⭐️ If you need to access an element of phrases/greetings.rs
from outside the module, you have to import greetings
module as a public module.
🔎 It’s unable to import child file modules of directory modules to
main.rs
, so you can’t usemod phrases::greetings;
frommain.rs
. But there is a way to importphrases::greetings::hello()
tophrases
module by re-exportinghello
tophrases
module. So you can call it directly asphrases::hello()
.
This allows you to present an external interface that may not directly map to your internal code organization. If it is still not clear, don’t worry; We discuss the usages of use
on an upcoming section in this post.
03. Crates
💭 Crates are bit similar to the packages in some other languages. Crates compile individually. If the crate has child file modules, those files will get merged with the crate file and compile as a single unit.
💭 A crate can produce an executable/ a binary or a library.src/main.rs
is the crate root/ entry point for a binary crate andsrc/lib.rs
is the entry point for a library crate.
▸ lib.rs file on the same executable crate
💡 When writing binary crates, we can move the main functionalities to src/lib.rs
and use it as a library from src/main.rs
. This pattern is quite common on executable crates.
💯 As I mentioned earlier, in here we use simplest examples to reduce the complexity of learning materials. But this is how we need to write
greetings/src/lib.rs
to make the code more testable.
📖 When importing a crate that has dashes in its name “like-this”, which is not a valid Rust identifier, it will be converted by changing the dashes into underscores, so you would write
extern crate like_this;
lib.rs
can link with multiple files.
▸ Dependency crate specified on Cargo.toml
When the code in the lib.rs
file is getting larger, we can move those into a separate library crate and use it as a dependency of the main crate. As we mentioned previously, a dependency can be specified from a folder path, git repository or by crates.io.
» Let’s see how to create a nested crate and use it as a dependency using folder path,
» If you want to use a library crate on multiple projects, one way is moving crate code into a git repository and use it as a dependency when required.
» The other way is uploading it to crates.io and use it as a dependency when needed.
🚧 First, let’s create a simple “Hello world” crate and upload it to crates.io .
💭 //! doc comments are used to write crate and module-level documentation. In other places we have to use /// outside of the block. And when uploading a crate to crates.io, cargo generates the documentation from these doc comments and host it on docs.rs.
💡 We have to add the description and license fields to Cargo.toml, otherwise we will get error: api errors: missing or empty metadata fields: description, license. Please see http://doc.crates.io/manifest.html
To upload this to crates.io,
01. We have to create an account on crates.io to acquire an API token
02. Then run cargo login <token>
with that API token and cargo publish
📖 This is how it describes on Cargo Docs with more details.
- You’ll need an account on crates.io to acquire an API token. To do so, visit the home page and log in via a GitHub account (required for now). After this, visit your Account Settings page and run the
cargo login
command specified.
Ex.cargo login abcdefghijklmnopqrstuvwxyz012345
- The next step is to package up your crate into a format that can be uploaded to crates.io. For this we’ll use thecargo package
sub-command.
- Now, it can be uploaded to crates.io with thecargo publish
command.
- If you’d like to skip thecargo package
step, thecargo publish
subcommand will automatically package up the local crate if a copy isn’t found already.
The name of our crate is test_crate_hello_world
. So it can be found on,
📦 https://crates.io/crates/test_crate_hello_world
📑 https://docs.rs/test_crate_hello_world
💯 crates.io supports readme files as well. To enable it, we have to add the readme field to Cargo.toml. Ex:
readme="README.md"
🏗️ Okay then, Let’s see how we can use this from an another crate.
By default, Cargo checks dependencies on crates.io . So we have to add only the crate name and a version string to Cargo.toml and then run cargo build
to fetch the dependencies and compile them.
04. Workspaces
When the code base is getting larger, you might need to work on multiple crates on the same project. Rust supports this via Workspaces. You can build, run tests or generate docs for all crates at once by running cargo
commands from the project root.
⭐️ When working on multiple crates at the same time, there is a higher possibility to having shared dependencies on crates. To prevent downloading and compiling the same dependency multiple times, Rust uses a shared build directory under the project root, while running cargo build
from the project root.
🔎 rust-lang/rust source folder is a good example for a workspace.
use
▸ Mainly use
keyword is used to bind a full path of an element to a new name. So user doesn’t want to repeat the full path each time.
▸ Another common usage of use
is importing elements to scope. Remember that, this is also bit similar to creating alias and using it instead of using the full path.
💡 By default,
use
declarations use absolute paths, starting from the crate root. Butself
andsuper
declarations make that path relative to the current module.
Same way use
keyword is used to import the elements of other crates including std
, Rust’s Standard Library.
We don’t need to use
extern crate std;
when usingstd
library. We will discuss more about this under Standard Library section.
💡 use
statements import only what we’ve specified into the scope, instead of importing all elements of a module or crate. So it improves the efficiency of the program.
▸ Another special case is pub use
. When creating a module, you can export things from another module into your module. So after that they can be accessed directly from your module. This is called re-exporting.
This pattern is quite common in large libraries. It helps to hide the complexity of the internal module structure of the library from users, because users don’t need to know/follow whole directory map of the elements of the library while working with them.
Rust Standard Library, Primitives and Preludes
⭐️ In Rust, language elements are implemented by not only std
library crate but also compiler as well. Examples,
- Primitives : Defined by the compiler and methods are implemented by
std
library directly on primitives. - Standard Macros : Defined by both compiler and
std
std
library has been has divided into modules, according to the main areas each covered.
⭐️ While primitives are implemented by the compiler, the standard library implements most useful methods directly on the primitive types. But some rarely useful language elements of some primitives are stored on relevant
std
modules. This is why you can seechar
,str
and integer types on both primitives andstd
modules.
🔎 When examine Rust’s source code, you can see that
src
directory is a workspace. Even though its having many library crates, by examine root Cargo.toml file you can easily identify that main crates arerustc
(compiler) andlibstd
(std
). Inlibstd/lib.rs
std modules have been re-exported viapub use
and the original location of most of std modules issrc/libcore
.
Few important std
modules are,
» std::io
- Core I/O functionality
» std::fs
- Filesystem specific functionality
» std::path
- Cross platform path specific functionality
» std::env
- Process’s environment related functionality
» std::mem
- Memory related functionality
» std::net
- TCP/UDP communication
» std::os
- OS specific functionality
» std::thread
- Native threads specific functionality
» std::collections
- Core Collection types
💯 Refer Rust Standard Library Documentation for more details.
⭐️ Even though Rust std
contains many modules, by default it doesn’t load each and every thing of std library on every rust program. Instead, it loads only the smallest list of things which require for almost every single Rust program. These are called preludes. They import only,
Preludes have been imported explicitly on
libstd/lib.rs
and the whole list can be seen onlibstd/prelude/v1.rs
.
⭐️ So technically, Rust inserts,
▸ extern crate std;
: into the crate root of every crate
▸ use std::prelude::v1::*;
: into every module
So you don’t need to import these each time.
The concept of preludes is quite common on Rust libraries. Even some modules inside std
crate (ex.libstd/io
) and many libraries (ex. diesel) are having their own prelude
modules.
⭐️ But preludes are used to create a single place to import all important components which are required while using the library. They do not load automatically unless you imported them manually. Only std::prelude
imports automatically in every Rust programs.
🚧 An example about how we can organize code while using structs and traits inside modules will be added after few days.
Ok, Let’s stop the fourth post of Learning Rust (& Gitbook)series in here. In this post I just tried to summarize about,
▸ Modules ▸ Crates ▸ Workspaces ▸ std modules and Preludes
🐣 I am a 🇱🇰 Web Developer who lives in 🇻🇳. So I am not a native English speaker and just learning Rust, If you found any mistake or something need to be changed, even a spelling or a grammar mistake, please let me know. Thanks.
Okay, see you on the next post :)
“It is never too late to be what you might have been.”
__ George Eliot
NEXT ▸ Smart Compiler ▸ Panicking ▸ Option and Result ▸ Unwrap & Expect ▸ Error and None Propagation ▸ Combinators ▸ Custom Error Types