Skip to content

Instantly share code, notes, and snippets.

@h4cc
Created July 28, 2017 07:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save h4cc/61f916295b806627088354345f7a0133 to your computer and use it in GitHub Desktop.
Save h4cc/61f916295b806627088354345f7a0133 to your computer and use it in GitHub Desktop.
Example how matching on elixir structs will make code more robust and refactorable with less possible errors at runtime.
# This is our struct with some fields.
defmodule User do
defstruct name: "julius", role: :user
end
defmodule UserManager do
# Matching on %User{} will ensure we will get a user.
# Matching on %User{name: name} will make the elixir compiler check that there is a name in user at compile time.
# Using a guard for new_role will ensure the type of data in our struct, so its important too.
# Updating the user with %User{user|role: new_role} will also ensure that all given fields exist at compile time.
# Downside: Need to write more code ...
# Upside: Easier refactoring. No possible ways to have runtime errors
def promote(%User{name: name, role: role} = user, new_role) when is_atom(new_role) do
IO.puts "Promoting user #{inspect name} from #{inspect role} to #{inspect new_role}"
%User{user|role: new_role}
end
# user can be any kind of struct here, even a map.
# There is no check what type of data new_role is.
# Using user.name and user.role will be accessed at runtime and can fail.
# Updating the user without using the struct can fail at runtime too.
# Downside: Refactoring is hard. Runtime errors possible.
# Upside: Less code
def promoto_not_matched(user, new_role) do
IO.puts "Promoting user #{inspect user.name} from #{inspect user.role} to #{inspect new_role}"
%{user|role: new_role}
end
end
# Helper for running as script.
defmodule Main do
def main() do
user = %User{}
admin1 = UserManager.promote(user, :admin)
IO.inspect admin1
admin2 = UserManager.promote(user, :admin)
IO.inspect admin2
end
end
Main.main()
@binajmen
Copy link

Don't ask me how I ended on a 7 years old gist. But here I am 😄 Day-3 on my Elixir learning curve.

I'm building a library to consume an external API I depend on. I know the structure of the responses. /api/v1/categories "should" always return something like:

[
  {
    "code": "expenses:food.restaurants",
    "defaultChild": false,
    "id": "7e88d58188ee49749adca59e152324b6",
    "parent": "067fa4c769774ae980435c76be328c0b",
    "primaryName": "Food & Drinks",
    "searchTerms": "food,lunch,snacks",
    "secondaryName": "Restaurants",
    "sortOrder": 45,
    "type": "EXPENSES",
    "typeName": "Expenses"
  },
  ...
]

My first attempt was to use Ecto to parse the data I receive:

defmodule Tink.Category do
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    field(:code, :string)
    field(:default_child, :boolean)
    ...
  end

  def parse(map) do
    %Tink.Category{}
    |> Ecto.Changeset.cast(map, [
      :code,
      :default_child,
      ...
    ])
    |> Ecto.Changeset.validate_required([
      :code,
      :default_child,
      :id,
      ...
    ])
    |> Ecto.Changeset.apply_action!(:validate)
  end
end

defmodule Tink.Category.Service do
  def list_categories() do
    res = Tink.Client.request(:get, "/api/v1/categories", %{})

    Enum.map(res.body, fn category ->
      Map.new(category, fn {k, v} -> {Macro.underscore(k), v} end)
      |> Tink.Category.parse()
    end)
  end
end

It is working fine. But I wonder if I could get rid of one deps (Ecto) and use the tools of Elixir like defstruct. Based on your gist, this is what I ended up with:

defmodule Tink.CategoryStruct do
  @enforce_keys [:code, :default_child, :id, :sort_order, :type, :type_name]
  defstruct [
    :code,
    :default_child,
    ...
  ]
end

defmodule Tink.CategoryStructBuilder do
  def new(
        %{
          code: code,
          default_child: default_child,
          id: id,
          sort_order: sort_order,
          type: type,
          type_name: type_name
        } = category
      ) do
    %Tink.CategoryStruct{
      code: code,
      default_child: default_child,
      id: id,
      parent: category.parent,
      ...
    }
  end

  def new(category) do
    IO.inspect(category)
    IO.puts("Invalid category")
  end
end

But the outcomes is not what I expected:

%{
  "code" => "expenses:house.other",
  "default_child" => true,
  "id" => "01f944531ab04cd3ba32a14cebe8a927",
  "parent" => "9308a083665741588b88d9160aedf968",
  "primary_name" => "Home Improvements",
  "search_terms" => nil,
  "secondary_name" => "Home Improvements Other",
  "sort_order" => 14,
  "type" => "EXPENSES",
  "type_name" => "Expenses"
}
Invalid category

I'm don't understand why the pattern matching is not working 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment