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. In this post, I won't go into theoretical details about functional programming. Instead, I'll implement and compare a concrete example to highlight the differences between thinking in OOP vs functional paradigms.
For example, I have a ToyRobot object in Ruby. In OOP terms, this object has:
- 3 attributes:
x,y, anddirection - Some actions:
move,turn left,turn right
Below is the code implemented in Ruby:
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
@y += 1
when :east
@x += 1
when :south
@y -= 1
when :west
@x -= 1
end
if @x < BOUNDS[:x].min || @x > BOUNDS[:x].max || @y < BOUNDS[:y].min || @y > BOUNDS[:y].max
raise "Out of bounds"
end
end
def right
case @direction
when :north
@direction = :east
when :east
@direction = :south
when :south
@direction = :west
when :west
@direction = :north
end
end
def left
# implement left
end
end
I'll build the same thing in Elixir. If you're new to Elixir, I encourage you to try implementing it yourself before checking my 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
So what are the differences between these two implementations? I think there are two important differences that highlight the different ways of thinking between OOP and functional languages:
1. Method vs Function
In Ruby, we have objects. Methods are functions that belong to a class. When you call an instance method, you can think of the ToyRobot taking an action like move or left:
robot = ToyRobot.new
robot.move
robot.left
In Elixir, we don't have objects. Modules are just used to group functions. When we call ToyRobot.move(robot), we're applying the move function to the robot's data:
robot = ToyRobot.new()
ToyRobot.move(robot)
ToyRobot.left(robot)
In Elixir function definitions, we need to pass all required data to the function as arguments, but the Ruby version doesn't need to. This is because, in OOP languages, objects know their state (the position of the robot in our case). In Elixir, functions just receive and transform data—taking the robot's position and returning a new one.
2. Mutable vs Immutable
In Ruby, every time we call a method, we change the state of the ToyRobot object:
robot = ToyRobot.new
robot.move # @x, @y change in the object
robot.report
# => {x: 0, y: 1, direction: :north}
robot.move # Continues to change the same object
robot.report
# => {x: 0, y: 2, direction: :north}
In Elixir, data is immutable. Every time we call a function, it returns a new struct and does not change the input:
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} # Original unchanged!
Elixir provides the pipe operator (|>) so we can call functions sequentially:
robot = ToyRobot.new()
|> ToyRobot.move()
|> ToyRobot.move()
|> ToyRobot.right()
|> ToyRobot.left()
In conclusion, the fundamental difference is in how we think about state:
- Ruby (OOP): "The object changes its own state"
- Elixir (Functional): "The function receives data and returns new data"
Top comments (0)