Blog Post: 10 Elixir gotchas

I meant to say that it’s not really something you run into often, if ever. I honestly don’t remember the context in which it came up and it was actually a coworker who brought it. I was then confused myself until it hit me. It’s really just a point of potential interest.

Is this also applicable to values we are setting in the config.exs like:

config :my_app, x: Enum.to_list(1..1_000_000)

And then calling for the “const”:

defmodule Foo do
  def foo, do: Application.get_env(:my_app, :x)
  def bar, do: Application.get_env(:my_app, :x)
end

Because this is usually the way I’m defining and using my constants…


Regarding the subject of constants, I think we are already all good in Elixir and here is my 2cts (biased because of my background in C)…

If I do, a = 2 * 3.14 * r, I guess everyone will get that this is pi… But not everyone will grab what 0x89504e47 is… So doing PNG_SIGNATURE=0x89504e47 is pretty sane… And that still would apply for pi PI=3.14159 so this allows to reuse it without introducing typos… (although for this example we have a better option of course by using :math.pi())
But if this was a regular variable, the risk to have it modified elsewhere in the codebase would be not null…

So often we use constants to make a given literal value being meaningful and to actually have it read-only.

So using functions (actually like pi() in :math) or even putting it as a config is exactly solving these two use cases.

IMHO, putting all the constants in one place like we do in Elixir apps within the config is the way to do it or within a module (I like to have MyApp full of my constants and magic numbers or submodule if I want to namespace them). This is my argument about being all good in Elixir.

Of course we can still use for one-file uses cases the module attributes (like we for fields in Schemas or like in test files with attributes, etc.).

But I can see the following addition (to the formatter at least), maybe we can add a module attribute to tell that the following function is intended to be a constant and then allowing for that function to be used without parentheses (and maybe bring some optimization as well regarding memory mapping).

defmodule Math do

  @constant #or whatever keyword
  def pi, do: 3.141592653589793
end

MyMath.pi # vs Math.pi() enforced by the formatter

# Some people don't like not having them all uppercase..
# But even if we allow to use uppercase only in this situation,
# Math.PI will definitely be confusing as a module in Elixirland
# Math.pi might also recall as a map field, which is semantically equivalent here, so win-win

Anyway, sorry for this long message and hope it makes sense…

The values you set in config.exs & co are stored in an ETS table, and so they are copied over to the caller process on each fetch. So in this case you might end up with many copies of a large list (e.g. if it is fetched by each request handler process).

Even without this issue, I personally find exs scripts confusing, and I advise avoiding them as much as possible. Can’t be done always, but they can be significantly shrunk.

When it comes to constants, i.e. giving a meaningful name to a magical value, my advice is to use local variable (if it is needed only in one function), or a named function placed in the module which is responsible for the related part of the functionality (i.e. not stashing all into one single module with constants).

4 Likes

Can you expand on that please? How? Can the config scripts be .ex and not .exs?

For all the concern about mutability of module attributes during compilation, consider that is the mechanism that lets ExUnit @tag s work. So, there’s an advantage too.

Something to help with constants in Elixir:

4 Likes

The point of the gotcha isn’t “module attributes bad” but “module attributes as a workaround for constants have some issues”. Much like a lot of the gotchas, the behavior is usually there for a reason - but that doesn’t mean it isn’t confusing.

I personally don’t understand in what scenario I’d like to have the “Erlang Term Ordering” behaviour though. Instead of comparing 2 values of different types returning a “random” (–> somewhat arbitrary order) boolean value I think I’d much prefer an exception, as I don’t see when you’d want that behavior. I’m sure I’m missing something, as it was designed by good people.

Off the top of my head, if we have a number of function heads that match different types of values, there may be a compiler optimization for efficient branching/jumps when recursing through long lists or other data structures like map keys?

I didn’t want to dive into a constant convo as I am prone to soapboxing buuuuuut I really like how “constants” work in Elixir, or rather the lack of them. It always drove me nuts when there would be a massive mess of constants at the top of a file. I also feel “public constants” are an anti-pattern so it’s great we don’t even have the choice. To me that is a function as stated in your article, @PragTob. Otherwise, I use config as @altdsoy mentioned as a lot of the time that’s what it is. I disagree with that tweet asking for attribute accessors—again, that is just a function. I’ve seen this in Elixir and don’t see the value:

@not_quite_pi 3.14
def not_quite_pi, do: @not_quite_pi

Perhaps I’m missing something, but I also saw this in Ruby and didn’t get it there either. Feels like cargo culting to me.

Regarding the mutability, as @gregvaughn says: it’s compile time mutability, so unless you have 100s of them and can’t see the forest for the trees, there’s pretty much no chance you’re going to accidentally redefine one. Otherwise, they don’t even exist at runtime (well, I know there is a way to make them stick around but it’s not ergonomic for business logic).

1 Like

The term ordering is essential for data structures such as maps, sets, ordsets, orddict, and others. Dynamic languages have heterogeneous collections, which means you can have keys, lists, and sets with distinct data types. Some of these data structures are more efficiently implemented by having a total order. If Erlang/Elixir only allowed you to compare the same data types, those data structures would have to define their “total” ordering, and it would ultimately be inefficient, especially for composite data types (e.g. storing structs inside sets, such as a set of dates).

The main source of confusion is because Elixir comparison operators are structural, not semantic. But there are good reasons for those as well. The docs go in detail over this: Kernel — Elixir v1.17.0-dev

The mutability of module attributes shouldn’t be a concern, really. Defining modules in Elixir is mutable a whole. defmodule, def, attrs are all mutable operations that define a module, functions, etc as you execute them. This what allows you, for example, to programmatically define a function. But, as meta-programming, those abilities are only really there at compile-time by design.

6 Likes

Thanks a lot as always @josevalim :green_heart: That makes sense, I’ll link it in the blog post and of course it’s already documented… maybe I should go and read the entire docs again at some point for all the great stuff that has been added!

I mean - there are other ways to do it but I think sharing some data/defaults isn’t too bad. Like - the default country, default payment method, the maximum age, maximum/minimum transaction amounts. Of course, better if you manage to contain them in one module - but it can be hard.
I mean, you can make an argument that all of these could instead be in a config - but the config are also constant-like (plus difference for different environment, which often means a lot).

That said, I agree that constants in Ruby being “public” by default I don’t like. In that sense, needing to “wrap” a module attribute in a function to make it accessible can be a boon imo :slight_smile:

1 Like

It’s not, just depends on the “type” of constant and it’s a whole other convo :sweat_smile:

I agree but I was specifically talking about creating a constant and then the only place it’s used is in a function that just returns it.

@PragTob (shameless plug) You might be interested in this micro lib I made, Cmp, with precisely this issue in mind. It should be safe, fast and should always do the right thing out of the box.

Regarding point 4. and is_map(1..10) == true, we decided to introduce a new is_non_struct_map/1 guard in order to be have a simple and blessed way of checking for this (vs is_map(term) and not is_struct(term)).

Thanks a lot for the amazing article!

5 Likes

Back when I worked with Ruby still, I just added .freeze after the constant’s definition. Helped my team back then much more often than I expected.

Thanks @sabiwara ! I was literally debating implementing the same thing (from a cursory look) and post a draft of it like: “Any reason not to do this?”. I was gonna name it Comparable, but I see that’s what you called the protocol and yeah, would have done the same. I had been wondering forever why it isn’t done like this/doesn’t exist. So, thanks a lot! :green_heart:

Awesome, sounds useful - thanks again for all your (& the core teams) hard work!

Pleasure + thanks for calling it out :dancer:

I like that (and rubocop suggests it) but it only really fixes a problem with mutability, not with all of them being “public” which isn’t always what you’d want (as I would often just extract constants for naming and even more to avoid wasteful allocations).

1 Like

Can we get a non_nil_atom guard too?

I had been thinking about this as well. nil, true and false being atoms is up for the next edition. I think it’s tougher, I can see non_nil but the booleans being in the mix is also somewhat confusing.

Since atoms should be known at compile time I think you’re usually better off making an explicit check against a list of known atoms though aka in [:a, :b, :c]

3 Likes

In a way they can. You can provide many of the values from the regular code (i.e. modules in lib). E.g. both, an Ecto repo and a Phoenix endpoint, can accept params as the first arg passed to start_link.

1 Like

Exactly. I rarely see the issue with atoms showing up because, when you want an atom, then you either want all atoms, or you expect some atoms, and then you list the ones you want.

Rather the opposite problem is more common: people using booleans to model problems, when atoms would be clearer. But boolean obsession/blindness is not an Elixir specific issue. :slight_smile:

How is this much different from maps and structs. You either wants all maps or you pattern match. Still ‘is_map_not_struct’ was added.

Nil is sometimes used as return value for not-existing or undefined (e.g. ecto) so an is_atom check is sometimes used to check if there is a result. Seems to me the guard is not aligned with the expectation of newcomers as mentioning this gotcha receives a lot of love.

  1. That nil, true and false are technically atoms is easy to miss as no colons are needed. Their special status might just as well make them excluded from is_atom imho. Then ‘is_atom’ simply means ‘is a colon keyword’ and debugging symbols align with the match syntax.

  2. Exceptions for true and false might be considered, but -I- have never seen that mistake.

  3. I wonder if excluding nil, false and true from is_atom would be a breaking change in the sense that (I think) most people probably did not want those values to match in the first place.

2 Likes