Elixir way of pattern-matching against constants in function signature

How is Elixir way of pattern matching against other modules attribute value in function signatures?

For example, let’s imagine we have module like the following:

defmodule Bar do
  @const 42
  def const, do: @const
end

Pretty self-explanatory - there’s a module Bar with an attribute of @const and it is accessible from other modules via Bar.const().

I would like to use that value of 42 in other function signatures without writing it. How would I do that?

I’ve tried so far:

defmodule Foo do
  def quux(Bar.const), do: IO.println("hi const!")
  def quux(_), do: IO.println("hi var!")
end

Compilation fails:

cannot invoke remote function Bar.const/0 inside a match

Okay, only values/variables are supported so I tried to be clever and use ^ like this:

defmodule Foo do
  @x Bar.const
  def quux(^@x), do: IO.println("hi const!")
  def quux(_), do: IO.println("hi var!")
end

Compilation still fails:

invalid argument for unary operator ^, expected an existing variable, got: ^@x

What I’ve currently done so far to achieve my goal is something like the following:

defmodule Foo do
  def quux(val) do
    if val == Bar.const do
      IO.println("hi const!")
    else
      IO.println("hi var!")
    end
  end
end

However, I don’t feel that this is an Elixir way. How should I approach this kind of a problem?

You’d have to do something like this using when:

defmodule Foo do
  @const Bar.const()

  def quux(val) when val == @const do
    IO.inspect("hi const!")
  end

  def quux(val), do: IO.inspect("hi val!")

The following works. Note the missing pin operator.

defmodule Foo do
  @x Bar.const

  def quux(@x), do: IO.println("hi const!")
  def quux(_), do: IO.println("hi var!")
end
4 Likes

See also this discussion that involves some similar code-shapes:

3 Likes

You can also use some unquote/1 magic:

defmodule Foo do
  def quux(unquote(Bar.const())), do: IO.println("hi const!")
  def quux(_), do: IO.println("hi var!")
end

But I would discourage you from doing such stuff as it make WTFs/s rate go dramatically up.

2 Likes

This would not work for any type of value though: only the ones that are equal to their AST (numbers, atoms, strings…).

unquote(Bar.const()) might have to be replaced by unquote(Macro.escape(Bar.const())) if we need to support these other cases.

This works nicely but it is worth mentioning there is a small difference with when val == @const if @const is or contains a map (also true for the unquote version above):

defmodule Foo do
  @x %{a: 1}

  @doc """
      iex> Foo.quux(%{a: 1, b: 2})
      :partial_match
      iex> Foo.quux(%{a: 1})
      :exact_match
  """
  def quux(x) when x == @x, do: :exact_match
  def quux(@x), do: :partial_match
end
1 Like

Late at the party post incoming:

Depends what you’re comparing. As @sabiwara pointed out, maps can catch you off-guard.

I would just use a macro-ish code generator and call it a day:

defmodule Bar do
  @constants %{
    42 => "const",
    220 => "MEGA const!"
  }

  def constants(), do: @constants
end

defmodule Foo do
  for {key, val} <- Bar.constants() do
    # This works fine but will be imprecise if you have constants that are maps
    def quux(unquote(key)), do: IO.puts("hi #{unquote(val)}!")

    # ...so do literal comparison if you're paranoid about constants being maps
    def quux(key) when key == unquote(key), do: IO.puts("hi #{unquote(val)}!")
  end

  def quux(_), do: IO.puts("hi <unknown>!")
end

I am in half-agreement with @hauleth here: sure you don’t have all the code at a glance (because the functions in the module are being generated at compile time) and that might be confusing. At the same time, basic stuff like iterating over a collection and using unquote on its elements to do basic code generation is not hard to do and comprehend.

Most Elixir devs I’ve met had no problem with basic macro usage. And many teams use the above technique to generate code and save themselves from error-prone boilerplate copy-pasting.