Chapter 3

Functions and Lambdas

Rugo has two kinds of callable things: named functions (`def`) and anonymous lambdas (`fn`). Both are first-class, but they serve different roles.

Rugo has two kinds of callable things: named functions (def) and anonymous lambdas (fn). Both are first-class, but they serve different roles.

Named Functions: The Workhorses

Define functions with def. Parentheses are optional for no-argument functions. Use early returns to keep logic flat.

def classify(score)
  if score >= 90
    return "A"
  end
  if score >= 80
    return "B"
  end
  if score >= 70
    return "C"
  end
  return "F"
end

puts classify(95)
puts classify(82)
puts classify(71)
puts classify(55)
A
B
C
F

Prefer early returns over deeply nested if/elsif chains. Each condition is a clear exit point — the function reads top to bottom.

Lambdas: Functions as Values

Lambdas are anonymous functions using fn...end. They can be stored in variables, passed as arguments, and returned from functions.

double = fn(x) x * 2 end
square = fn(x) x * x end

puts double(5)
puts square(4)
10
16

One-line lambdas are perfect for callbacks. The last expression is the implicit return value — no return needed for simple expressions.

Higher-Order Functions

Pass lambdas to functions for map, filter, and transform patterns. This is how idiomatic Rugo replaces what other languages do with built-in iterators.

def my_map(f, arr)
  result = []
  for item in arr
    result = append(result, f(item))
  end
  return result
end

def my_select(f, arr)
  result = []
  for item in arr
    if f(item)
      result = append(result, item)
    end
  end
  return result
end

nums = [1, 2, 3, 4, 5, 6]
doubled = my_map(fn(x) x * 2 end, nums)
puts doubled

evens = my_select(fn(x) x % 2 == 0 end, nums)
puts evens
[2 4 6 8 10 12]
[2 4 6]

Build these once, use them everywhere. Real Rugo libraries attach each, map, and select as methods on collection objects (see Chapter 8).

Closures Capture by Reference

When a lambda references a variable from its enclosing scope, it captures it. This is the foundation of factory functions.

def make_adder(n)
  return fn(x) x + n end
end

add5 = make_adder(5)
add10 = make_adder(10)
puts add5(3)
puts add10(3)
8
13

Each call to make_adder creates a new closure with its own n. The lambda remembers the value even after make_adder returns.

Lambda Dispatch Tables

Store lambdas in hashes for clean dispatch logic. Dot access makes the calls look like method invocations.

ops = {
  add: fn(a, b) a + b end,
  sub: fn(a, b) a - b end,
  mul: fn(a, b) a * b end
}

puts ops.add(10, 3)
puts ops.sub(10, 3)
puts ops.mul(10, 3)
13
7
30

This pattern replaces switch statements in other languages. Need a new operation? Add another entry to the hash.

Function Composition

Combine small lambdas into larger ones. This is a natural fit for data transformation pipelines.

double = fn(x) x * 2 end
inc = fn(x) x + 1 end

def compose(f, g)
  return fn(x) f(g(x)) end
end

double_then_inc = compose(inc, double)
puts double_then_inc(5)
11

compose(inc, double) reads as "increment after doubling" — double(5) gives 10, then inc(10) gives 11.

Break and Next in Loops

Use break to exit early when you've found what you need. Use next to skip items that don't matter.

# Find the first negative number
nums = [3, 7, 2, -1, 5, -3]
for n in nums
  if n < 0
    puts "First negative: #{n}"
    break
  end
end

# Skip nils in a dataset
data = [1, nil, 3, nil, 5]
total = 0
for item in data
  if item == nil
    next
  end
  total += item
end
puts "Total: #{total}"
First negative: -1
Total: 9

These two keywords handle 90% of the cases where other languages need exceptions, iterators, or complex loop conditions.