Day 2/100: Common Programming Concepts

Day 2/100: Common Programming Concepts

Lessons from The Rust Book, chapter 3

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 to isize and u8 to usize.

  • The default integers are i32 and u32 depending on the context as well as usize 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.

References