Need naming help for macro that allows for a function to be evaluated at compile time or runtime

I’m working on a library that allows me to control wether a function is either evaluated at runtime or compile-time and need help with naming. Here’s how I’m currently using it:

In order to make our application easier to test, we are building it in layers and allowing each layer to be swapped out with a Mox backend. A layer is usually made up of 3 main files:

# lib/accounts.ex
defmodule Accounts do
  @moduledoc """
  Main accounts API
  """

  alias Accounts.Account

  @behaviour Accounts.Backend

  @type id :: String.t

  @spec fetch_account(id) :: {:ok, Account.t} | {:error, :not_found}
  def fetch_account(id) do
    backend().fetch_account(id)
  end

  ##
  # This allows me to swap backends during test, but also hits ETS during every
  # function call in prod.
  ##
  defp backend do
    Application.get_env(:accounts, :backend, DefaultBackend)
  end
end

# lib/accounts/backend.ex
defmodule Accounts.Backend do
  @moduledoc """
  Behaviour for implementing an Accounts backend.
  """
  alias Accounts.Account

  @callback fetch_account(id) :: {:ok, Account.t} | {:error, :not_found}
end

# lib/accounts/default_backend.ex
defmodule Accounts.DefaultBackend do
  @moduledoc false

  alias Accounts.Account

  @behaviour Accounts.Backend

  @spec fetch_account(id) :: {:ok, Account.t} | {:error, :not_found}
  def fetch_account(id) do
    # Account fetching implementation goes here
  end
end

Then in my apps that use the Accounts module as a dependency, they can swap out the default backend with a mock backend for testing

# test/test_helper.exs of a higher layer app
ExUnit.start()

Mox.defmock(Bookstore.MockAccounts, for: Accounts.Backend)
Application.put_env(:accounts, :backend, Bookstore.MockAccounts)

For me, the swapping behavior only needs to happen in the test envirionment and in :dev and :prod, I’d rather not take the ETS hit. That’s where I wrote the defbackendp macro so I could do this:

defmodule Accounts do
  @moduledoc """
  Main accounts API
  """

  import BackendUtils

  alias Accounts.Account

  @behaviour Accounts.Backend

  @type id :: String.t

  @spec fetch_account(id) :: {:ok, Account.t} | {:error, :not_found}
  def fetch_account(id) do
    backend().fetch_account(id)
  end

  ##
  # In test - this calls Application.get_env at runtime
  #
  # In dev/prod, - this calls Application.get_env at compile-time and builds a·
  # private function that returns the value as it was at compile time.
  ##
  defbackendp backend do
    Application.get_env(:accounts, :backend, DefaultBackend)
  end
end

And here is the macro:

defmodule BackendUtils do
  @moduledoc """
  Defines the `defbackendp` macro, which allows a function to be evaluated at
  compile time for a given environment.
  """

  @compiled_envs [:dev, :prod]

  defmacro defbackendp(name, do: block) do
    if Mix.env in @compiled_envs do
      {result, _} = Code.eval_quoted(block, [], __CALLER__)

      quote do
        defp unquote(name) do
          unquote(result)
        end
      end
    else
      quote do
        defp unquote(name) do
          unquote(block)
        end
      end
    end
  end
end

Finally, here is my question… It seems like there are more situations where the ability to control if a function is evaluated at runtime or compile-time is useful than just my swapping backends pattern. If I wanted to name this generically instead of BackendUtils.defbackendp, what is a good name for it? I’m considering publishing it to hex so I can use it on various projects, but I’d like it to be useful others besides me.

2 Likes

One of the 2 great errors of programming, Naming things! ^.^;

You could always let github auto-generate a name for it, it’s nonsensical, but eh? :slight_smile:

What about a name like defprecompute_if(name, condition, do: block)?

Then that would be called like:

@compiled_envs [:dev, :prod]
defprecompute_if backend, (Mix.env in @compiled_envs) do
  Application.get_env(:accounts, :backend, DefaultBackend)
end

And you could even re-write your macro slightly and introduce more magic naming:

@compiled_envs [:dev, :prod]
defprecompute if Mix.env in @compiled_envs, backend) do
  Application.get_env(:accounts, :backend, DefaultBackend)
end

but it is not that much more readable, so I think this version is not better.

1 Like