DEV Community

hungle00
hungle00

Posted on

From Ruby OOP to Elixir Functional by Example

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, and direction
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Elixir provides the pipe operator (|>) so we can call functions sequentially:

robot = ToyRobot.new()
|> ToyRobot.move()
|> ToyRobot.move()
|> ToyRobot.right()
|> ToyRobot.left()
Enter fullscreen mode Exit fullscreen mode

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)