From Ruby OOP to Elixir Functional by Example
Source: Dev.to
Ruby implementation
One of the most effective ways to learn functional programming is to convert the same features you’ve written in an OOP language to functional style. Below is a simple ToyRobot object in Ruby with three attributes (x, y, direction) and actions (move, turn left, turn right).
class ToyRobot
attr_reader :x, :y, :direction
DIRECTIONS = [:north, :east, :south, :west]
BOUNDS = {x: 0..10, y: 0..10}
def initialize
@x = 0
@y = 0
@direction = :north
end
def place(x, y, direction)
if DIRECTIONS.include?(direction)
@x = x
@y = y
@direction = direction
else
raise "Invalid direction"
end
end
def report
{x: @x, y: @y, direction: @direction}
end
def move
case @direction
when :north then @y += 1
when :east then @x += 1
when :south then @y -= 1
when :west then @x -= 1
end
if @x > BOUNDS[:x].max || @y > BOUNDS[:y].max
raise "Out of bounds"
end
end
def right
case @direction
when :north then @direction = :east
when :east then @direction = :south
when :south then @direction = :west
when :west then @direction = :north
end
end
def left
# implement left
end
end
Elixir implementation
Below is the same robot expressed in Elixir. If you’re new to Elixir, try implementing it yourself before looking at the code.
defmodule ToyRobot do
@directions [:north, :east, :south, :west]
@bounds_x 0..20
@bounds_y 0..20
defstruct x: 0, y: 0, direction: :east
def new() do
{:ok, robot} = place(0, 0, :east)
robot
end
def place(_x, _y, direction) when direction not in @directions do
{:error, :invalid_direction}
end
def place(x, y, _direction) when x not in @bounds_x or y not in @bounds_y do
{:error, :out_of_bounds}
end
def place(x, y, direction) do
{:ok, %ToyRobot{x: x, y: y, direction: direction}}
end
def report(%ToyRobot{x: x, y: y, direction: direction}) do
{x, y, direction}
end
def right(%ToyRobot{direction: direction} = robot) do
new_direction =
case direction do
:north -> :east
:east -> :south
:south -> :west
:west -> :north
end
%ToyRobot{robot | direction: new_direction}
end
def left(%ToyRobot{direction: direction} = robot) do
# implement right
end
def move(%ToyRobot{x: x, y: y, direction: direction} = robot) do
{new_x, new_y} =
case direction do
:north -> {x, y - 1}
:east -> {x + 1, y}
:south -> {x, y + 1}
:west -> {x - 1, y}
end
if new_x in @bounds_x and new_y in @bounds_y do
%ToyRobot{robot | x: new_x, y: new_y}
else
robot
end
end
end
Key Differences
Method vs Function
-
Ruby (OOP): Methods belong to a class and operate on the object’s internal state.
robot = ToyRobot.new robot.move robot.left -
Elixir (Functional): Modules group functions; you pass the data explicitly.
robot = ToyRobot.new() ToyRobot.move(robot) ToyRobot.left(robot)
In Elixir, every required piece of data must be supplied as an argument, whereas Ruby methods can access the object’s state implicitly.
Mutable vs Immutable
-
Ruby: Methods mutate the object’s state.
robot = ToyRobot.new robot.move # @x, @y change robot.report # => {x: 0, y: 1, direction: :north} -
Elixir: Data structures are immutable; functions return new structs.
robot = ToyRobot.new() # => %ToyRobot{x: 0, y: 0, direction: :east} new_robot = ToyRobot.move(robot) # => %ToyRobot{x: 1, y: 0, direction: :east} robot # => %ToyRobot{x: 0, y: 0, direction: :east} # unchanged
Elixir’s pipe operator (|>) makes chaining transformations concise:
robot = ToyRobot.new()
|> ToyRobot.move()
|> ToyRobot.move()
|> ToyRobot.right()
|> ToyRobot.left()
Conclusion
The fundamental difference lies in how state is handled:
- Ruby (OOP) – “The object changes its own state.”
- Elixir (Functional) – “The function receives data and returns new data.”