πŸ”§

Functions & Methods β€” What Changes When def Becomes fn

Return types, semicolons, and the familiar "last expression is the return" rule

In Ruby, defining a function needs no type for arguments or returns. Rust requires both.

# Ruby
def greet(name)
  "Hello, #{name}!"
end

// Rust
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

-> followed by the return type. No return value (Ruby's nil return) is -> () but can be omitted.

Semicolon = return or not

Both Ruby and Rust share the "last expression is the return value" rule. But in Rust, adding a semicolon makes it a statement, not a return.

fn add(a: i32, b: i32) -> i32 {
    a + b    // no semicolon β†’ return value
}

fn add_broken(a: i32, b: i32) -> i32 {
    a + b;   // semicolon β†’ statement, returns () β†’ compile error!
}

This is genuinely confusing at first. In Ruby, semicolons are just statement separators with no semantic meaning. In Rust, they determine whether something gets returned.

Early return

Just like Ruby, you can return early.

fn check_age(age: u32) -> &'static str {
    if age < 18 {
        return "minor";  // early return needs semicolon
    }
    "adult"  // last expression, no semicolon
}

return uses a semicolon. Last expression returns without one. This pattern is Rust convention.

Argument passing β€” value vs reference

In Ruby, you never think about how arguments are passed. In Rust, ownership means you must decide: borrow or move.

// borrow β€” original kept
fn print_name(name: &str) {
    println!("{}", name);
}

// ownership move β€” original unusable after call
fn take_name(name: String) {
    println!("{}", name);
}

let name = String::from("sehwa");
print_name(&name);  // borrow, name still usable
take_name(name);     // move, name unusable after this

Most of the time, borrow with &. "I'm only reading, no need to transfer ownership."

Closures β€” inline functions

Ruby's blocks/lambdas are closures in Rust. The pattern for passing them as arguments is nearly identical.

# Ruby
[1, 2, 3].map { |x| x * 2 }

// Rust
vec![1, 2, 3].iter().map(|x| x * 2).collect::<Vec<_>>();

|x| corresponds to Ruby's { |x| }. Pipes (|) instead of curly braces to wrap arguments.

Key Points

1

Define as fn name(arg: Type) -> ReturnType { }

2

No semicolon = return value, with semicolon = statement β€” this is key

3

Use return keyword + semicolon for early returns

4

Pass arguments by borrowing (&) or moving ownership

Pros

  • Function signature alone tells you input/output types
  • Same "last expression is return" rule as Ruby speeds up adaptation

Cons

  • Semicolon mistakes causing compile errors are frequent early on
  • Borrow/ownership decisions in function design are a new concept from Ruby

Use Cases

Converting Ruby method chaining to Rust function composition Implementing callback patterns with closures