Macro Monday - Share your macros!

Happy Macro Monday to everyone!

I was wonder what kinds of crafty macros you think are awesome and have come to rely upon.

I’ll go first:

Sigil m


  defmacro sigil_m({:<<>>, _line, [string]}, []) do
    spec =
      string
      |> String.split()
      |> Stream.map(&String.to_atom/1)
      |> Enum.map(&{&1, {&1, [], nil}})

    {:%{}, [], spec}
  end

This one I copy-pasted from the web (can’t remember the origin) and it’s truly a killer in readability and speed of development.

I use it in many places, but on tests & liveview really shines:

Tests

test "happy path", ~%{user: user, project: project, task_1: task_1, task_2: task_2} do
test "happy path", ~m{user project task_1 task_2} do

Liveview

%{current_user: current_user,  project: project} = socket.assigns
~m{current_user project} = socket.assigns

Maybe Case

This one is “my own creation” (quotes apply as I received tons of help).

Think tap/1 meets case.

  defmacro maybe_case(prev, do: block) do
    quote do
      prev = unquote(prev)

      case prev do
        unquote(block ++ [{:->, [], [[{:_, [], nil}], nil]}])
      end

      prev
    end
  end

The use I have for this is the following. In my context layer I have all this functions such as schedule_task, unschedule_task etc, which in principle all look something like this:

Multi.new()
|> Multi.run(...)
|> Multi.run(...)
|> Multi.run(...)
|> Repo.transaction()
|> case do
   {:ok, %{action: ...}} -> {:ok, ...}
   {:ok, %{action_2: ...}} -> {:ok, ...}
   {:error, %{action: ...}} -> {:error, ...}
end

That’s great until you want to introduce side effects like logging and pubsubing. I could of course capture the result of the transaction, make conditional statements for publish & log and then shape the response.

results = Multi.new()
  |> Multi.run(...)
  |> Multi.run(...)
  |> Multi.run(...)
  |> Repo.transaction()

case results do
  {:ok, _, _} -> PubSub.publish_change()
  {:error, _, _} -> Logger.error()
end

case results do
   {:ok, %{action: ...}} -> {:ok, ...}
   {:ok, %{action_2: ...}} -> {:ok, ...}
   {:error, %{action: ...}} -> {:error, ...}
end

But being a pipe-man myself, I decided I needed my code to look like this:

Multi.new()
|> Multi.run(...)
|> Multi.run(...)
|> Multi.run(...)
|> Repo.transaction()
|> maybe_case do
  {:ok, _} -> PubSubs.publish_change(...)
  {:error, _} -> Logger.error(...)
end
|> case do
   {:ok, %{action: ...}} -> {:ok, ...}
   {:ok, %{action_2: ...}} -> {:ok, ...}
   {:error, %{action: ...}} -> {:error, ...}
end

Now it’s your turn!

6 Likes

I don’t like macros. They’re obscure and inscrutable and irritating, and they get everywhere.

That said, I have two macros that I use to add type information to my code, my typed struct and typed Ecto schema macros. The latter is ugly and incomplete, but the former I can link for you: lib/geo_therminator/typed_struct.ex · 8fa72cbfa5b0402c46c53e13e5e994bf5a390f89 · Mikko Ahlroth / GeoTherminator · GitLab

I use it like this then:

defmodule Register do
  deftypedstruct(%{
    register_name: String.t(),
    register_value: {integer(), 0},
    timestamp: DateTime.t()
  })
end

It defines the struct, a @type t, and @enforce_keys for me all in one go. I copy it to every new project I start.

6 Likes