Skip to main content
  1. Posts/

Rust basics: functions and types

·598 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Rust functions are explicit at the boundary and flexible inside the body. You spell out parameter and return types, then let inference do most of the local work.

That balance is one of Rust’s better design choices. Signatures stay readable, but the code inside them does not turn into annotation noise.

Function syntax
#

A basic Rust function looks like this:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

There are three details worth remembering:

  • parameters always have types
  • the return type comes after ->
  • the final expression becomes the return value if it has no semicolon

Returning values
#

The last expression is usually enough:

fn square(n: i32) -> i32 {
    n * n
}

You can still use return when an early exit is clearer:

fn clamp(x: i32, min: i32, max: i32) -> i32 {
    if x < min {
        return min;
    }

    if x > max {
        return max;
    }

    x
}

That is common in validation and error handling paths.

Statements versus expressions
#

Rust leans heavily on expressions:

let result = {
    let base = 10;
    base + 5
};

The block evaluates to 15. If you add a semicolon to the last line, the block evaluates to ().

This distinction matters because many Rust constructs are really “value-producing blocks.”

Common scalar types
#

You do not need to memorize the entire type system to get started. A small set covers most beginner code:

  • i32, i64: signed integers
  • u32, u64, usize: unsigned integers
  • f32, f64: floating-point values
  • bool: true or false
  • char: a single Unicode scalar value

Examples:

let port: u16 = 8080;
let ready: bool = true;
let initial: char = 'R';
let ratio: f64 = 0.75;

Strings: &str versus String
#

This is one of the first distinctions every Rust learner hits.

&str is a borrowed string slice:

let language: &str = "Rust";

String is an owned, growable string:

let mut name = String::from("Ada");
name.push_str(" Lovelace");

As a rule of thumb:

  • use &str for string data you only need to read
  • use String when you need ownership or mutation

Type inference keeps local code short
#

Rust infers types from context whenever it can:

let n = 10;
let name = String::from("m58");

You only need an explicit annotation when inference would be ambiguous or when the annotation improves readability:

let ids: Vec<u64> = Vec::new();
let timeout_ms: u64 = 5_000;

Beginner code often benefits from a few extra annotations. They are not mandatory, but they can make intent easier to read.

Associated functions and methods
#

Not every function is free-standing. Rust also supports associated functions and methods through impl blocks.

struct User {
    name: String,
}

impl User {
    fn new(name: String) -> Self {
        Self { name }
    }

    fn greeting(&self) -> String {
        format!("hello, {}", self.name)
    }
}
  • new is an associated function because it does not take self
  • greeting is a method because it takes &self

This is how most Rust types expose behavior.

A realistic example
#

fn describe_port(port: u16) -> String {
    if port == 443 {
        "https".to_string()
    } else if port == 80 {
        "http".to_string()
    } else {
        format!("custom:{port}")
    }
}

This function shows a few common patterns:

  • explicit input and output types
  • expression-based branching
  • returning an owned String

Summary
#

  • Rust function signatures are explicit about parameters and return types.
  • The last expression usually serves as the return value.
  • Type inference keeps most local bindings short.
  • The types you will use constantly are numbers, bool, char, &str, and String.

Once those pieces feel normal, reading Rust APIs gets much easier.

Related

Rust basics: control flow

·665 words·4 mins
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values. That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.

Rust basics: let, mut, and shadowing

·481 words·3 mins
Rust variables are really bindings. Once that clicks, let, mut, and shadowing stop feeling like syntax trivia and start feeling like design choices. Rust pushes you toward immutability by default. That is not just style. It reduces accidental state changes and makes data flow easier to follow.

Rust basics: structs and enums

·587 words·3 mins
Structs and enums are the core of Rust data modeling. Structs group fields together. Enums describe a value that can be one of several well-defined variants. If you are coming from languages where data modeling is mostly “objects everywhere”, Rust feels different at first. It prefers explicit data shapes plus separate behavior.