Intermediate

Functions in Ruby

Learn how to define and use methods in Ruby—parameters, default and keyword arguments, scope, recursion, and blocks, procs, and lambdas—with Docker-ready examples

In Ruby, the unit of reusable behavior is the method. Because everything in Ruby is an object, what other languages call a “free function” is really a method defined on an object—and a method defined at the top level of a file becomes a private method on Object, callable from anywhere. Ruby leans on this object model rather than a separate notion of standalone functions.

Ruby is multi-paradigm, blending object-oriented, functional, and imperative styles. That shows up clearly in how it handles reusable behavior: you get classic named methods with parameters and return values, but you also get blocks, procs, and lambdas—first-class chunks of code you can pass around, store in variables, and call later. This functional capability is what powers Ruby’s famously expressive iterators like each, map, and select.

This tutorial covers defining and calling methods, working with positional, default, keyword, and variable-length arguments, how Ruby handles return values and scope, writing recursive methods, and using blocks, procs, and lambdas as first-class code objects.

Defining and Calling Methods

A method is defined with the def keyword and closed with end. Ruby has an implicit return: the value of the last expression evaluated is returned automatically, so an explicit return is usually optional.

Create a file named functions.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# A simple method with no parameters
def greet
  "Hello from Ruby!"   # last expression is returned implicitly
end

# A method with two parameters
def add(a, b)
  a + b
end

# Parentheses are optional when calling
puts greet
puts add(3, 4)

# An explicit return can exit early
def absolute(n)
  return -n if n < 0
  n
end

puts absolute(-15)
puts absolute(15)

Calling a method does not require parentheses, though they improve clarity when arguments are passed. Note return -n if n < 0—Ruby’s trailing if modifier lets you guard an early return on a single, readable line.

Default and Keyword Arguments

Ruby supports default parameter values so callers can omit arguments. It also supports keyword arguments, where parameters are named at the call site, making calls self-documenting and order-independent.

Create a file named arguments.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Default positional argument
def power(base, exponent = 2)
  base ** exponent
end

puts power(5)      # uses default exponent of 2
puts power(2, 10)  # overrides the default

# Keyword arguments (named, order-independent)
def describe(name:, role: "developer")
  "#{name} is a #{role}"
end

puts describe(name: "Matz", role: "creator")
puts describe(role: "maintainer", name: "Alice")
puts describe(name: "Bob")   # role falls back to its default

Keyword arguments declared with a trailing colon (name:) and no default are required; Ruby raises an error if they are missing. Those with a value (role: "developer") are optional. String interpolation with #{...} embeds the argument values directly into the result.

Variable-Length Arguments

Ruby can collect an arbitrary number of arguments using the splat operator (*) for positional arguments and the double splat (**) for keyword arguments. This is how methods like puts accept any number of values.

Create a file named splat_args.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# *numbers collects all positional arguments into an array
def sum(*numbers)
  numbers.reduce(0) { |total, n| total + n }
end

puts sum(1, 2, 3)
puts sum(10, 20, 30, 40, 50)
puts sum   # no arguments -> empty array -> 0

# **options collects keyword arguments into a hash
def configure(**options)
  options.map { |key, value| "#{key}=#{value}" }.join(", ")
end

puts configure(host: "localhost", port: 3000, ssl: true)

Inside sum, numbers is a regular Array, so all of Ruby’s array methods are available—here reduce folds the values into a single total. Inside configure, options is a Hash. These collection methods take a block (the { ... } part), which we explore below.

Scope and Recursion

Ruby method definitions create a new local scope. Local variables from outside a method are not visible inside it—Ruby deliberately isolates methods to avoid surprising action at a distance. Recursion, where a method calls itself, works naturally for problems with a self-similar structure.

Create a file named scope_recursion.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
message = "outer scope"

def show_scope
  # The outer `message` is NOT visible here; this is a separate local
  message = "inner scope"
  puts message
end

show_scope
puts message   # unchanged: methods cannot see or modify outer locals

# Classic recursion: factorial
def factorial(n)
  return 1 if n <= 1
  n * factorial(n - 1)
end

puts factorial(5)

# Recursion: nth Fibonacci number
def fib(n)
  return n if n < 2
  fib(n - 1) + fib(n - 2)
end

puts fib(10)

The two message variables are completely independent because the method has its own scope. The factorial method recurses until it hits the base case n <= 1, and fib sums the two preceding values until n is below 2.

Blocks, Procs, and Lambdas

Here is where Ruby’s functional side shines. A block is an anonymous chunk of code passed to a method, written with do...end or {...}. A method can run the block with yield. For storing code in a variable and passing it around, Ruby offers procs and lambdas—both are objects you call with .call.

Create a file named higher_order.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# A method that yields to a block, running it 3 times
def repeat(times)
  i = 1
  while i <= times
    yield i   # hand control to the block, passing the counter
    i += 1
  end
end

repeat(3) { |n| puts "Iteration #{n}" }

# A lambda: a first-class function stored in a variable
square = ->(x) { x * x }
puts square.call(6)

# Higher-order: pass a lambda to a method via &
def apply_twice(value, &operation)
  operation.call(operation.call(value))
end

puts apply_twice(3, &square)   # square(square(3)) = 81

# Built-in iterators take blocks directly
doubled = [1, 2, 3, 4].map { |n| n * 2 }
puts doubled.inspect

evens = [1, 2, 3, 4, 5, 6].select { |n| n.even? }
puts evens.inspect

The repeat method uses yield to invoke whatever block the caller provides. The lambda ->(x) { x * x } is Ruby’s compact arrow syntax for an anonymous function. Prefixing a parameter with & converts a block to a proc object (and vice versa), which is how apply_twice receives and calls it. Methods like map and select are themselves block-taking methods—this is everyday functional programming in Ruby.

Running with Docker

Run each example using the official Ruby Alpine image—no local Ruby installation needed.

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull ruby:3.4-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby functions.rb
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby arguments.rb
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby splat_args.rb
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby scope_recursion.rb
docker run --rm -v $(pwd):/app -w /app ruby:3.4-alpine ruby higher_order.rb

Expected Output

Running functions.rb:

Hello from Ruby!
7
15
15

Running arguments.rb:

25
1024
Matz is a creator
Alice is a maintainer
Bob is a developer

Running splat_args.rb:

6
150
0
host=localhost, port=3000, ssl=true

Running scope_recursion.rb:

inner scope
outer scope
120
55

Running higher_order.rb:

Iteration 1
Iteration 2
Iteration 3
36
81
[2, 4, 6, 8]
[2, 4, 6]

Key Concepts

  • Implicit return — A method returns the value of its last evaluated expression; an explicit return is only needed to exit early.
  • Flexible arguments — Ruby supports positional, default, and keyword arguments, plus the splat (*) and double-splat (**) operators to collect variable-length arguments into an array or hash.
  • Keyword arguments are self-documenting — Named arguments make call sites readable and order-independent; those without defaults are required.
  • Methods have isolated scope — A method cannot see outer local variables, which prevents accidental shared state and surprising side effects.
  • Recursion is natural — With a clear base case, methods can call themselves to solve self-similar problems like factorial and Fibonacci.
  • Code is a first-class value — Blocks, procs, and lambdas let you pass behavior around. yield runs a block, & converts between blocks and proc objects, and .call invokes a stored callable.
  • Functional iterators — Built-in methods like map, select, and reduce take blocks, making functional-style data transformation idiomatic everyday Ruby.

Running Today

All examples can be run using Docker:

docker pull ruby:3.4-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining