Don't use structs, use records instead!
Tags: elixir
Published: 2023-05-25 18:04:26

Structs are omnipresent in Elixir projects. And for good reasons, they're great, they have numerous advantages over maps.

1. Autocompletion
1. Dot Syntax 
1. Enforce keys
1. Dialyzer friendly
1. Pattern matching

I use them everywhere. I try to only use maps when the keys are dynamic values.

But I have one issue with them. Something that I believe is a MUST-HAVE for the maintainability of a project:

> Struct are public, they can't be private

This probably raise 2 questions in your head: 

## What is a private structure?
A private structure is a structure that can only be CRUD (create/read/update/delete) from a single module. The module must expose accessors to manipulate it. 

## What are the benefits?
> **Decoupling**
>
> Modify the struct with peace of mind. It will only impact current module. No risk of breaking code from other places.

> **Enforce business rule at a single place**
>
> Don't worry about someone else overriding a business rule in some part of the code because she/he created a struct by her/him-self.

## The solution: records to the rescue

[Records](https://hexdocs.pm/elixir/1.14.5/Record.html) exist since the beginning of Elixir but no one talks about them. As you might have peeked in the reference, the `Record` module exposes a `defrecord` macro but more importantly a `defrecordp` macro...

The `defrecordp` macro allow us to create a private record. Hooray!

### The syntax

Record syntax is a bit weird because the macro generates 3 macro to manipulate the record. 

```elixir
Record.defrecord(:user, [:name, :age])
```

generates:

1. `user/0` will create the struct with default values 
1. `user/1` will create the struct from given values 
1. `user/2` will either update a struct or access a field

[See examples on the reference page](https://hexdocs.pm/elixir/1.14.3/Record.html#defrecord/3-examples)

## Demonstration: TMNT Game

I'll demonstrate the benefits with an example. Let's say that my business rule is to be able to play a game with 1 of the 4 famous ninja turles.

I'll present you the same example twice but first coded with a struct and secondly coded with a record.

### The struct version

```elixir
defmodule Turtle do
  defstruct [:weapon, :name, :color]

  @type color() :: :blue | :red | :purple | :orange
  @type t() :: %__MODULE__{
          weapon: String.t(),
          name: String.t(),
          color: color()
        }

  ######################################################################
  @spec new_by_color(color()) :: t()
  def new_by_color(:blue),
    do: %__MODULE__{color: :blue, name: "Leonardo", weapon: "Katana"}

  def new_by_color(:red),
    do: %__MODULE__{color: :red, name: "Raphael", weapon: "Sai"}

  def new_by_color(:purple),
    do: %__MODULE__{color: :purple, name: "Donatello", weapon: "Staff"}

  def new_by_color(:orange),
    do: %__MODULE__{color: :orange, name: "Michelangelo", weapon: "Nunchaku"}

  ######################################################################
  @spec battle_cry(t()) :: String.t()
  def battle_cry(t) do
    color = turtle_color(t.color)
    "#{t.name}: #{color}Cowabunga!"
  end

  ######################################################################
  defp turtle_color(:blue), do: IO.ANSI.blue()
  defp turtle_color(:red), do: IO.ANSI.red()
  defp turtle_color(:purple), do: IO.ANSI.magenta()
  defp turtle_color(:orange), do: IO.ANSI.yellow()
end
```

```elixir
defmodule Game do
  @spec play(Turtle.color()) :: :ok
  def play(color) do
    player = Turtle.new_by_color(color)
    tick(player)
  end

  defp tick(turtle) do
    IO.puts(Turtle.battle_cry(turtle))
  end
end

```

Looks good, no? Everything looks great, dialyzer pass. It's easy to read and easy to use.


![Game is played in a shell](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/af3miljm1rsmtf4teyi2.png)


Now... Imagine months pass and a coworker, fan of Dumas, creates this function in `Game.ex`: 

```elixir
def play2() do
  player = %Turtle{color: :blue, name: "Porthos", weapon: "Balizarde"}
  tick(player)
end
```

This code just broke my business rule. And nothing prevented that! How annoying! Who knows the impact of this change?


![Porthos yelling cowabunga](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ti12t88owlqqsqblr7rk.png)




## The record version

```elixir
defmodule Turtle do
  require Record

  Record.defrecordp(:turtle, [:weapon, :name, :color])

  @type color() :: :blue | :red | :purple | :orange
  @type t() :: record(:turtle, 
          weapon: String.t(), 
          name: String.t(),
          color: color()
        )

  ######################################################################
  @spec new_by_color(color()) :: t()
  def new_by_color(:blue),
    do: turtle(color: :blue, name: "Leonardo", weapon: "Katana")

  def new_by_color(:red),
    do: turtle(color: :red, name: "Raphael", weapon: "Sai")

  def new_by_color(:purple),
    do: turtle(color: :purple, name: "Donatello", weapon: "Staff")

  def new_by_color(:orange),
    do: turtle(color: :orange, name: "Michelangelo", weapon: "Nunchaku")

  ######################################################################
  @spec battle_cry(t()) :: String.t()
  def battle_cry(t) do
    color = turtle_color(turtle(t, :color))
    name = turtle(t, :name)
    "#{name}: #{color}Cowabunga!"
  end

  ######################################################################
  defp turtle_color(:blue), do: IO.ANSI.blue()
  defp turtle_color(:red), do: IO.ANSI.red()
  defp turtle_color(:purple), do: IO.ANSI.magenta()
  defp turtle_color(:orange), do: IO.ANSI.yellow()
end

```

```elixir
defmodule Game do
  @spec play(Turtle.color()) :: :ok
  def play(color) do
    player = Turtle.new_by_color(color)
    tick(player)
  end

  defp tick(turtle) do
    IO.puts(Turtle.battle_cry(turtle))
  end
end
```

As you can see, the code is very similar. `Game.ex` did not change at all.

Now... let's try to recreate the same scenario as before. Months pass and a coworker tries to add the `play2/0` function from above... Impossible! There's no way to create a turtle record from outside the `Turtle.ex` module.

> Actually there is a way by modifying the tuple directly, which is something no sane person will attempt.

Obviously, this example is very explicit but I believe it makes my point. In a real world example, the implicit coupling is much more subtle and usually creates friction when maintaining the code.

> For the curious, [the internal representation of a record is a tuple](https://www.erlang.org/doc/reference_manual/records.html). Whereas structs are maps. Which is yet another benefit in the record side. Don't take my word for it, read [Maps or Records?](https://www.erlang.org/doc/efficiency_guide/maps.html#maps-or-records-) for the details.

So what do you think? 

EDIT: Forgot to mention, there is a `@opaque` attribute. But unfortunately, it does not prevent the creation of the struct from outside the module. It only forbid to peek at the structure.

EDIT2: Please check the comments section, it's worth it.