sb logoToday I Learned

10 posts by davelucia @davydog187

Living with-out

There is a tendency in Elixir to reach for the with macro to handle control flow.

def fetch_token(type, params \\ []) do
  with params <- Keyword.merge(required_get_token_params(type), params),
       {:ok, client} <- OAuth2.Client.get_token(client(type), params) do
    {:ok, client}
  else
    err -> err
  end
end

At first glance, this code looks succinct, clean, and readable. Then you realize two refactors

With returns the unmatched value by default

def fetch_token(type, params \\ []) do
  with params <- Keyword.merge(required_get_token_params(type), params),
       {:ok, client} <- OAuth2.Client.get_token(client(type), params) do
    {:ok, client}
  end
end

If there is no else condition, you don’t need to double match on the last clause.

def fetch_token(type, params \\ []) do
  with params <- Keyword.merge(required_get_token_params(type), params) do
       OAuth2.Client.get_token(client(type), params)
  end
end

After these two refactors, you realize that you never needed with in the first place!

def fetch_token(type, params \\ []) do
  params = Keyword.merge(required_get_token_params(type), params)

  OAuth2.Client.get_token(client(type), params)
end

Now, this may seem overly pedantic, but many bugs can hang out in the plain sight of verbose code. Eliminating branching in your code is a great strategy for reducing complexity.

Register modules for lookup with persistent term

There have been several times in my years with Elixir that I’ve found a need to define a collection of modules that can be looked up as a group using some form of tagged dispatch.

Imagine an Event module that has two fields name and data. For event name, there will be a different shape of data. When you persist these events, you may want to dispatch to a different Ecto.load function or some other means to cast your data. By listing the modules, and filtering on an exported function, this can be done at runtime!

defmodule Event.Registry do
  @doc """
  Loads all `Event` modules into persistent term
  """
  @spec load() :: :ok
  def load do
    {:ok, modules} = :application.get_key(:my_app, :modules) 
    :persistent_term.put(__MODULE__, Enum.filter(modules, &function_exported?(&1, :__event_name__, 0)))
  end

  @doc """
  Looks up the `Event` module that exports the given `name`
  iex> load()
  iex> lookup(:hello)
  Event.Hello
  """
  @spec lookup(name :: atom()) :: module() | nil
  def lookup(name) when is_atom(name) do
    :persistent_term.get(__MODULE__) |> Enum.find(fn module -> module.__event_name__() == name end)
  end
end

In my production version, I use the event name in the key so that we can optimize away the Enum.find.

Avoid variables in your LiveView leex templates

In my phoenix templates, I have a tendency to use variables, especially for functions where I want to return multiple values. For example

# Template
<section>
  <% {color_class, indicator} = status_indicator(@match) %>
  <p class="<%= color_class %>">
    <%= indicator %>
  </p>
</section>

# View 
def status_icon(match) do
  case Match.status(match) do
    :prematch -> {"text-yellow-500", "prematch"}
    :live -> {"text-green-500", "live"}
  end
end

This is fine for a normal template, but with Phoenix LiveView, you’re running into a LiveEEX pitfall.

Avoid defining local variables, except within for, case, and friends

Using this method makes Phoenix LiveView opt out of change tracking, which means sending data over the wire everytime.

Instead, use multiple functions or call into another template:

# Multiple functions
<section>
  <p class="<%= status_indicator_color(@match) %>">
    <%= status_indicator(@match) %>
  </p>
</section>

# If your operation is expensive, do it once and call into another template 
<section>
  <%= render MatchView, "status_indicator.html", status: Match.status(@match) %>
</section>

# other template
<p class="<%= status_color(@status) %>">
  <%= status_indicator(@status) %>
</p>

# View
def status_color(status) do
  case status do
    :prematch -> "text-yellow-500"
    :live -> "text-green-500"
end

def status_indicator(status) do
  to_string(status)
end

Use Makeup to display data in your Phoenix UI

Here at Simplebet, we work on a product that is primarily centered around data. For our internal tools, we often want to be able to view the contents of raw messages for inspection, debugging, and testing. By combining Phoenix templates with the Makeup source code highlighter, it couldn’t be easier.

To get started, add makeup and makeup_elixir to your project. (Fun fact, Makeup is the tool that ex_doc uses to generate that beautifully printed source code in all Elixir documentation)

 {:makeup, "~> 1.0"},
 {:makeup_elixir, "~> 0.15.0"},

Then you can render your data like this

<div class="m-2 overflow-hidden shadow-lg">
    <style>
        <%= raw Makeup.stylesheet(Makeup.Styles.HTML.StyleMap.friendly_style()) %>
    </style>
    <%= raw Makeup.highlight(inspect(@message.message, pretty: true)) %>
</div>

The best part, Makeup has a list of stylesheets that you can choose from. I chose “friendly”, but there are many more. Enjoy!

Working with nested associations in LiveView

Creating forms of nested associations in LiveView can be intimidating and a head-scratcher at first. It turns out it is actually very easy!

In my sports example, I have a Match which has many PlayersInMatch, which has many PlayersInQuarter. We start with a changeset around match Ecto.Changeset.change(match)with everything preloaded.

The trick is, in order to update everything in the handle_event callback, you need to make sure that the id of all the associations is present in the form, and you do this by rendering hidden_inputs_for(assoc) at each level.

<%= for player <- @players do %>
    <%= hidden_inputs_for(player) %>
    <li class=""><%= player_name(player) %></li>
        <%= for piq <- inputs_for(player, :players_in_quarter) do %>
            <%= hidden_inputs_for(piq) %>
            <li class="border pl-2 py-1"><%= number_input piq, :points %></li>
        <% end %>
    </li>
<% end %>

When the callback is called, you will get all of the nested associations in the Match. Then it is simple as

 def handle_event("validate", %{"match" => params}, socket) do
   changeset = 
    socket.assigns.match
    |> Ecto.Changeset.cast(params, [])
    |> Ecto.Changeset.cast_assoc(:players_in_match, with: &PlayerInMatch.update_changeset/2)
  {:noreply, assign(socket, :changeset, changeset)}
end

When to use the Decimal library over floats? 🤔

Today I was wondering why we were using the Decimal library instead of my favorite faulty friend, the IEEE 754 floating-point number. Well, it says it right in the readme.

Arbitrary precision decimal arithmetic.

But what is arbitrary precision, Dave?! 🥱

Well, http://0.30000000000000004.com/ can explain it better than I can, but let me illustrate with an example!

iex(7)> 0.1 + 0.2
0.30000000000000004

🤢🤮

No fear, the Decimal library provides arbitrary precision floating points for us!

iex(1)> Decimal.add(Decimal.from_float(0.1), Decimal.from_float(0.2))
#Decimal<0.3>

Avoid nesting configuration

Often, I will see configuration nested under some thematic element, rather than the configuration’s intended usage. Let’s imagine that I want to mock out my mailer during tests, so I’ll store the actual mailer as a module attribute at compile time, and fallback to the actual module.

# config/test.exs
import Config

config :my_blog, :content_management, [mailer: MockMailModule, minimum_words: 200]

While this configuration makes sense thematically, the usage is going to be very different.

def MyBlog.Marketing do
  @mailer Application.compile_env(:my_blog, :content_management)[:mailer] || MailModule

  def send_marketing_email do
    @mailer.email("hi")
  end
end

This is fine, and it definitely works, but it would be simpler if we didn’t nest our configuration, and modeled it around the usage pattern.

# config/test.exs
import Config

config :my_blog, :content_management_mailer, MockMailModule
config :my_blog, :content_management_minimum_words, 200

Now we can leverage the default argument of compile_env/3

def MyBlog.Marketing do
  @mailer Application.compile_env(:my_blog, :content_management_mailer, MailModule)

  def send_marketing_email do
    @mailer.email("hi")
  end
end

Performing magic in Elixir

Ancient Magic

iex> [109, 97, 103, 105, 99]
'magic'

In Elixir, there is a data type known as a charlist that can be confusing for beginners. Charlists are a list of codepoints (ASCII integer values), that can be rendered as a readable string.

Modern Magic

iex> <<109, 97, 103, 105, 99>>
"magic"

Bitstrings are also a sequence of codepoints, but packed together as a contiguous sequence of bits.

Which magic should I prefer?

If you’re writing Elixir, you almost always want the bitstring version when dealing with strings, since they are more memory efficient. If you’re using the String module, you’re dealing with bitstrings under the hood. As an Elixir developer, it is rare that you will need a charlist, and it will typically be due to interfacing with Erlang code.

For displaying bitstrings and charlists in IEx, read more in the Elixir Getting Started guide and the Inspect.Opts documentation.

Revealing the magic

As a poor magician, I will reveal my trick

iex> inspect("magic", binaries: :as_binary)
"<<109, 97, 103, 105, 99>>"

Use context functions for writing tests

When writing ExUnit tests that require setup, use describe blocks and context functions to your advantage!

def App.FooTest do
  use ExUnit.Case

  describe "when there is a bar" do
    setup :single_bar
    
    test "you can get a bar", %{bar: bar} do
     assert %App.Bar{} = App.Context.get_bar(bar.id)
    end
  end

  describe "when there is a fancy bar" do
    setup :single_bar
    @tag bar_params: %{color: "orange"}
    test "you can get a fancy bar", %{bar: bar} do
      assert %App.Bar{} = App.Context.get_bar(bar.id)
    end
  end

  def single_bar(context) do
    params = context[:bar_params] || %{a: 1}
    {:ok, bar} = App.Context.create_bar(params)
    %{bar: bar}
  end
end

Don't use Map functions on structs

While it may seem like a good idea, Map functions should not be used on Elixir structs, as they can lead to some violations of the data structure. Specifically, you can use the Map API to add keys that don’t exist on the struct.

defmodule Test do
  defstruct [:foo]
end

test = %Test{}

# Adds a field :bar that doesn't exist on the struct
%{__struct__: Test, bar: :a, foo: nil} = Map.put(test, :bar, :a)

Instead, use the Map update syntax that validates that you’re using an existing key. If it doesn’t exist, it will throw a KeyError

%{test | bar: :a}
** (KeyError) key :bar not found in: %Test{foo: nil}
    (stdlib 3.12.1) :maps.update(:bar, :a, %Test{foo: nil})
    (stdlib 3.12.1) erl_eval.erl:256: anonymous fn/2 in :erl_eval.expr/5
    (stdlib 3.12.1) lists.erl:1263: :lists.foldl/3