garrison

garrison

Global constants and pattern matching

A library I’m working on has a lot of functionality related to string prefixes (technically binaries, but they’re mostly strings). I have to add prefixes to keys a lot, and I have to pattern match them back off a lot.

Like:

defp prefix(key), do: "foo/" <> key
defp is_foo("foo/" <> key), do: true
defp is_foo(_key), do: false

And so on. But a lot of this functionality is embedded in application code, in anonymous functions, in Enum.maps, variable assignments, and so on. It’s everywhere.

I want to standardize these prefixes across the codebase going forward. They are already standardized, of course, but I have module attributes scattered across a couple dozen modules when I want them to be in one place.

There is, essentially, a lot of code like this, with the attributes duplicated across many modules:

@prefix "foo"
defp something(@prefix <> rest), do: rest

The docs recommend using public functions as constants instead of attributes, but this doesn’t work because I also need to use them for pattern matching everywhere.

I can think of two approaches off the top of my head:

First, use a module with a __using__ macro to inject the same set of attributes into every module. This would be acceptable for my use case, but it seems kinda cursed.

Second, replace the attributes with macros that inject the constants into the expressions. Essentially, the function-as-constant approach but with a macro instead. This sounds more sane to me, and is what I’ll probably do, but I figured I should seek some guidance in case I’ve overlooked something. This approach is conspicuously absent from the docs I linked.

Most Liked

cmo

cmo

I don’t hate the __using__ method for those cases that are truly global and/or want to be used in function heads.

It is a little less discoverable but it doesn’t take me too long to see the use MyModuleAttrubuteConstants at the top of the module and remember what I’ve done.

mudasobwa

mudasobwa

Creator of Cure

Use global public macros as constants then.

defmodule Prefixes do
  defmacro prefix(prefix \\ "foo", rest) do
    case __CALLER__.context do
      :match -> 
        quote generated: true, do: unquote(prefix) <> unquote(rest)
    end
  end
end

defmodule Usage do
  import Prefixes, only: [prefix: 1]

  def something(prefix(key)), do: key
end

Resulting in:

iex(2)> Usage.something "ggg"
** (FunctionClauseError) no function clause matching in Usage.something/1    
    
    The following arguments were given to Usage.something/1:
    
        # 1
        "ggg"
    
    iex:6: Usage.something/1
    iex:5: (file)
iex(5)> Usage.something "fooggg"
"ggg"
mudasobwa

mudasobwa

Creator of Cure

Well, Elixir macros are being expanded during compile-time and the AST they return is being directly injected into a resulting beam code. That said, the runtime would have zero impact compared to something("foo" <> rest) because _this is exactly the code the VM machine would see.

Duting the compilation time, yeah, it’ll gorge a couple of processor ticks, but you’d never notice this.

dimitarvp

dimitarvp

Then I would go for the __using__ approach but would not hard-code the values there; I’d make them part of the application’s config and just use Application.compile_env in the code that’s injected via the macro. That would help with the single source of truth problem.

garrison

garrison

This is contextual enough that I would not expect you to understand, but in this particular case the values are “implementation details” in such a way that using config seems like the wrong choice. It would be like defining magic numbers for a file format using config options - not quite right. These values are not going to change.

Anyway, I don’t love the __using__ approach because I feel like it’s less discoverable. At some point someone is going to read my code and wonder where @prefix came from, and it’s not going to be defined. I don’t like that.

If I use individual macros at least they will have a clear definition somewhere discoverable.

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement