Today, I learnt about common programming concepts in rust and developed a simple program to test my new knowledge. We will look at:
Keywords
Variables & Constants
Data types
Functions and
Control flow
Keywords
Just like in every language, keywords are words that are reserved for Rust's use only. E.g. for, fn, loop, while, etc.
Variables & Constants
Variables
A value bound to a name is called a variable in rust. They are immutable by default. However, the mut
keyword is used to indicate mutability.
Sample code:
let mut name = "DaveSaah";
Properties of variables:
They can be made mutable using the
mut
keyword.They cannot be declared in a global scope.
Constants
Constants are values that are bound to a name and are not allowed to change. The mut
keyword cannot be used by constants; they are always immutable.
A constant can be declared using the const
keyword, and the type of the value must be annotated. Example:
const PI :u32 = 3.14
Properties of constants:
They can be declared in any scope including the global scope.
By convention, their names are all uppercase and group of words are spaced with underscores. E.g. LATENT_HEAT.
Constants can only be set to a constant expression, not the result of a function call or any other value that can only be computed at runtime.
They are valid for the entire time a program runs within the scope they were declared in.
Tip from the Rust Book: "Try naming hardcoded values used throughout your program as constants. It helps to convey the meaning of the value. It also limits updates to one line, should a constant value be changed."
Shadowing
Shadowing is the process of declaring a new variable with the same name as the previous variable. Let's break it down with an example:
let age = "34"; // previous variable
let age = 45; // new variable shadowing the previous one
What are the benefits of shadowing?
Shadowing allows you to perform a few transformations on a value but have the variable remain immutable after the transformations.
Though
mut
makes a variable mutable, it doesn't affect the variable type. On the other hand when a variable is shadowed, they can be of different types. This feature is particularly useful to keep consistent variable names even after type conversions.
Data Types
Every value in Rust is of a certain type. Data types in Rust are grouped under two: Scalar and compound types.
Scalar Data Types
A scalar data type represents a single value. There are 4 main ones: integers, floating point numbers, booleans and characters.
Integers
Integers are numerical values with no fractional part. They can be signed (i.e. having i
prefix) or unsigned (i.e. having the u
prefix).
Integers vary from
i8
toisize
andu8
tousize
.The default integers are
i32
andu32
depending on the context as well asusize
for indexing collections.
Floating Point Numbers
They are numbers with a fractional part. They can be single precision,
f32
or double precision,f64
.The default type for floating point numbers is
f64
.
Booleans
They are values that represents either true
or false
.
Characters
They are single quoted unicode values. Unicode values can be alphabets, numbers, symbols, and emojis.
Compound Data Types
Compound data types group multiple values of different types into one. The two primitive compound types in Rust are tuples and arrays.
Tuples
Tuples group different values of different data types into a single compound type. They are created by listing values in a comma-separated list inside parentheses.
Sample code:
fn main() {
let tup = (500, 5.5, "Hello");
}
Single values from a tuple can be accessed via pattern matching destructuring or by index. Sample code:
fn main() {
let tup = (500, 5.5, "Hello");
let (year, height, greeting) = tup; // destructuring
// accessing values via index
let year = tup.0;
let height = tup.1;
let greeting = tup.2;
}
Arrays
Arrays are like tuples, but they only group values of the same data type.
They have a fixed length. As a result, they cannot grow or shrink in size during runtime after it has been declared.
Arrays are created by listing comma separated values in square brackets.
Sample code:
let arr = [1, 2, 3, 4, 5]; // declaring an array
let one = arr[1]; // accessing a value from an array.
Usefulness of arrays:
Arrays are useful if you want data to be stored on the stack rather than the heap.
They ensure that you have a fixed number of elements throughout the program's lifetime.
Functions
The meaning of a function in Rust is consistent with other programming languages :).
Function definition syntax:
fn function_name(param :type, param :type ...) -> return_type {
// body
}
Rust only cares that your function is defined. It does not matter if it is above or below the
main
function.
In Rust, the return value of a function is explicitly stated with the return keyword, or the final expression in the function definition is implicitly used.
For example:
fn get_age() -> u32 {
return 54;
}
fn get_height() -> f64 {
5.5 // an expression that is implicitly used as a return value
}
A Note on Statements & Expressions
A statement is an instruction that performs an action, but does not return a value.
- E.g. Creating and assigning a variable.
Expressions on the other hand evaluate to a resulting value.
- E.g. Basic math operations (1+1), calling a function or a macro.
Adding a semicolon to an expression turns it into a statement.
Sample code (just a basic example to get the idea):
1 + 1 // expression
let x = 3; // statement
Control Flow
Control flow is handled through the following channels:
If expressions.
Loop (yes, loop :)).
While loop.
For loop.
The definitions and usage of the above constructs is the same in other programming languages. The differences might be the syntax and the new loop
construct.
If-expressions
It allows the branching of code depending on conditions.
Syntax:
if boolean_expression {
// code
} else if boolean_expression {
// code
} else {
// code
}
Rust does not convert non-boolean values into a boolean. So the typical,
if variable_name {}
syntax doesn't work. The conditions for if-expressions are strictly boolean expressions.
Another use-case for if-expressions is in assigning values to variables. A sample code speaks louder than words :)
let condition = true;
let x = if condition {
3
} else {
5
};
In the example above, the value of x is 3 if the condition is true, else 5 is assigned.
Important!!!: All the values in the if-expression block must be of the same type else the Rust compiler will throw an error.
The following results in an error:
let condition = true;
let x = if condition {
3
} else {
"I am x. Find me!"
};
Loop
A loop indicates that a block of code should run forever unless you explicitly tell it to stop.
Sample code:
fn main() {
loop {
println!("Hello world");
}
}
While loop
It allows a block of code to run while a certain condition is true.
Syntax:
while boolean_expression {
// code
}
For Loop
It is used to iterate over a collection or a range of values.
Some sample usage of for loops:
let elements = [1, 2, 3, 4, 5];
// to iterate over a collection
for item in a.iter() {
// code
}
// to iterate over a range of values
for i in 1..4 { // 1, 2, 3
// code
}
The Range
The syntax for range is begin...end
, where begin and end are integers. begin
is inclusive, and end
is exclusive.
Other Notes
Large values can be separated with underscores. E.g. 1_000_000.
Rust supports numeric operations such as addition, subtraction, division, multiplication, and modulus. However, operations can only happen on the same data type. E.g. Integers cannot be multiplied by floating point numbers.
The annotated type for booleans is
bool
.The annotated type for characters is
char
.By convention, variables, constants and function names use snake_case syntax.
//
are used for comments.///
are used for documentation.New scopes can be created using curly braces,
{}
.
Concluding Thoughts
The implementation of common programming concepts in Rust are not so different from other languages I've used in the past. There are some elements of python's dynamic typing seen in the concept of shadowing
. The "immutability by default" design choice is not that bad. It's actually good. You are able to understand which variables are expected to change and which ones do not, and that saves much time when trying to understand the flow of values in your program.
It's exciting.
Tomorrow's chapter is about something entirely new. Some say it's brillant, others say its a principle that already existed, but only enforced in Rust. We'll never know until we understand ownership in Rust :).
Thanks, and I'll see you tomorrow.