[055] Maps, Part 2

Introducing structs and demonstrating how they can take the place of public Records.

Subscribe now

Maps, Part 2 [05.13.2016]

In the last episode, we introduced the new Map data type in Elixir. In today's episode, we're going to look at structs. A struct is a tagged map that allows developers to define default values for keys. This provides a lot of the convenience that we used to use Records for. Let's look at it a bit further.

Basic implementation

To define a struct, you just need to define an __struct__ function on a given module. This should return a map:

defmodule SomeStruct do
  def __struct__ do
    %{foo: "bar"}
  end
end

You can use the struct like you're creating a map:

iex(3)> %SomeStruct{}
%SomeStruct{foo: "bar"}

Behind the scenes, it's just a map with a __struct__ key whose value is the SomeStruct module:

iex(6)> %{__struct__: SomeStruct, foo: "baz"}
%SomeStruct{foo: "baz"}

Elixir provides a defstruct convenience macro to simplify defining a struct for a given module:

defmodule Person do
  defstruct first_name: nil, last_name: "Dudington"

  def name(person) do
    "#{person.first_name} #{person.last_name}"
  end
end
iex(8)> josh = %Person{first_name: "Josh"}
%Person{first_name: "Josh", last_name: "Dudington"}
iex(9)> Person.name(josh)
"Josh Dudington"

You can use the map update syntax to update structs, and they will verify that the data you're updating is a struct of the appropriate type:

iex(10)> %Person{josh | last_name: "Adams"}
%Person{first_name: "Josh", last_name: "Adams"}
iex(11)> map = %{first_name: "Josh"}
%{first_name: "Josh"}
iex(12)> %Person{map | first_name: "Lance"}
** (BadStructError) expected a struct named Person, got: %{first_name: "Josh"}

Let's go ahead and see what using structs to define the BEAM Toolbox data types would look like:

defmodule CategoryGroup do
  defstruct [:name, :categories]
end
defmodule Category do
  defstruct [:name, :projects]
end
defmodule Project do
  defstruct [:name, :website, :github]
end

Then defining the default data would look pretty similar to what we had before:

data = [%CategoryGroup{name: "Testing", categories: [
  %Category{name: "Integration Testing", projects: [
    %Project{name: "Amrita", website: "http://amrita.io", github:
"http://github.com/josephwilk/amrita"},
    %Project{name: "Hound", website: "https://github.com/HashNuke/hound",
github: "http://github.com/HashNuke/hound"},
  ]}
]}]
testing = Enum.at(data, 0)
integration = Enum.at(testing.categories, 0)
amrita = Enum.at(integration.projects, 0)

Summary

Today, we just had a quick look at structs, which are meant to be the primary means of providing polymorphic dispatch moving forward. They provide some compile-time guarantees about valid keys for operations you're going to do, as well as runtime exceptions if the data being passed through their update functions is not of the appropriate shape.

They're going to be exremely useful for modelling data of various types. I'm looking forward to using them where I would have previously used Records.

See you soon!