5

I find it a little misleading and confusing that while Elixir boasts immutability, that immutability is buried between layers of mutable abstractions.

For example,

if I have the code:

foo = {:cat, "Puff"}
bar = foo
foo = {:cat, "Pepper"}

I can expect bar to remain {:cat, "Puff"} But foo's changing value can make it difficult to reason about.

When I first read of module attributes, I thought they would provide some solution, but it was pointed out to me that this is perfectly valid code:

defmodule Cats do
   @cat "Puff"
   def foo, do: @cat

   @cat "Pepper"
   def bar, do: @cat
end

Would it be possible to rely on function definitions remaining constant? Eg.

defmodule Cats do
   def foo, do: "Puff"
   def bar, do: "Pepper"
end

Could such code be considered idiomatic? Are there any reasons I shouldn't do this?

Is there any other sort of entity I can declare as having some value without any possibility of some other code giving that entity a new value?

Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • 1
    Just a quick note. In the first example, foo is not changing but being rebind. In the underlying implementation the second foo, is actually a different variable (value) than the first foo. In Erlang VM, there is no way to change the value of an identifier (variable). Elixir provides this sugar-like syntax to make it more sweet on the surface! (Which is more familiar for developers coming from other programming languages) – Kaveh Shahbazian Aug 10 '19 at 21:00
  • @KavehShahbazian, That seems too much like splitting hairs. But my concern isn't whether I am able to change the memory at the initial memory location, but rather the value obtained by consuming some entity labeling a memory location. I want a way to guarantee that if I consume a labeled entity, the result will always be the same. – Brian Kessler Aug 10 '19 at 21:16
  • In both cases, `foo` and `bar` work as you expect. Nothing is going to change. Maybe I am not understanding your concern correctly. – Kaveh Shahbazian Aug 10 '19 at 21:54
  • 2
    @KavehShahbazian “sugar-like syntax” — not at all. It was a well-grounded decision, explained by José [here](http://blog.plataformatec.com.br/2016/01/comparing-elixir-and-erlang-variables/). – Aleksei Matiushkin Aug 11 '19 at 10:19
  • @AlekseiMatiushkin, cheers for the link; that really cuts to the heart of my concern. So far as I understand it, neither Erlang nor Elixir provide any native way to prevent rebinding of variables. Are there any 3rd party packages that do this? Is this something we could do ourselves with "reasonable" effort? – Brian Kessler Aug 11 '19 at 10:54
  • Erlang does not permit rebinding at all in the first place, as it is clearly stated in the post I linked. As for Elixir, I wrote the whole answer below explaining why the concept of constants is plain wrong and should be considered an anti-pattern. There are surely ways to prevent this using metaprogramming, or natively if one declares a [module attribute](https://hexdocs.pm/elixir/Module.html#register_attribute/3) with `accumulate: true`, opting-out overwrites. Still, one should not use anti-patterns brought from other languages (where those might be not considered as such.) – Aleksei Matiushkin Aug 11 '19 at 11:31
  • Constants are anti-patterns? I don't see your answer below even suggesting that. Please expound on that idea further. – Brian Kessler Aug 11 '19 at 15:47
  • Did my best on elaborating. It is extremely hard to argue for onion and garlic are not needed when cooking a cake, though. – Aleksei Matiushkin Aug 12 '19 at 05:01

2 Answers2

4

Could such code be considered idiomatic?

No.

Are there any reasons I shouldn't do this?

A ton.

You do not need constants in BEAM in the first place. Just use "Puff" and "Piffles" everywhere. New memory won’t be allocated. This is also true for any term. The last bug with tuples was fixed in 1.6.

So there is no need to create constants at all.

If you want to shorten the name to call a value by, you can define a macro, that will be inlined by compiler everywhere. This is more efficient than calling a function (slightly, but still.)


After reading through all the comments, I got an impression that we might be trapped by the terminology here. There are many things people call constants in CS. E. g. in Java there are final int i = 42; and public enum Math { PI }. There are constants like in Ruby CONST Pi = 3.14159265. And there are constants like in Javascript const i = 42;.

They all differ. Java enum is once set, never changed. Ruby allows resetting. Java final and Javascript const are mostly the same, the subtle difference might be omitted without a loss of generality.

So far so good. You seem to talk about Javascript const. Elixir has a greatly shaped scoping. One cannot accidentally modify anything. But there is a rebinding. One cannot rebind the variable from inside another scope, though.

iex|1 ▶ foo = 42
iex|2 ▶ if true, do: foo = :bar
#⇒ warning: variable "foo" is unused
iex|3 ▶ foo
#⇒ 42

Assigning foo inside a block, macro/function call, closure, any different scope would not modify the original variable.

The only possible rebinding may occur within the same scope. If we are talking about local variables in functions, the chances one would accidentally rebind the variable not on purpose are nearly zero (and it is covered by this great article by José.) If we are talking about module attributes, they are private to the module scope, inlined by compiler, inaccessible from outside. One might use both accumulated and not-accumulated module attributes and there is a way to declare a real constant in javascript way by declaring the attribute being accumulated and refer to the head of the list, but nobody uses it that way, because module attributes are usually declared on top of the module and it’s very easy to grasp whether there are duplicated names.

Long story short, const in Javascript saves developers from shooting their legs in the cases that are simply impossible in Elixir because of immutability and scoping.

Show any example of the code where you think you need a const and I’ll show how it should be refactored to be an idiomatic Elixir without using anything const-like.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • 1
    Constants are nice to use, not just for memory, but to enforce consistency and provide a single point where you can change a value which will be reused multiple times in an application. Replacing the same value throughout the application can be tedious and error prone (what if some other value is the same by coincidence). Macros might be a good approach assuming the value can be known at compile time... – Brian Kessler Aug 11 '19 at 10:39
  • If the value cannot be known at compile-time, it cannot be called constant by definition. If one needs the same constant in different places of the application, I would condider refactoring, because it it surely a code smell. – Aleksei Matiushkin Aug 11 '19 at 10:42
  • maybe I've worked with EcmaScript 6 too much, but I rather buy into the concepted of a scope constant, where "constant" can mean just for the life of the entity, not the life of the application, so a constant can have a scope of a module, a function, or just a block. – Brian Kessler Aug 11 '19 at 10:52
  • There are no “lifecycles” nor “entities” in Elixir (unless we are talking about processes, which is definitely out of scope here.) Put a value in the _protected_ ETS by the application and read from there from any other process, if you need a true constant value. – Aleksei Matiushkin Aug 11 '19 at 11:35
2

I think you answered your own question. My understanding is that using module attributes as constants is the standard practice, and is even recommended in the getting started guide, but as you pointed out, module attributes can be re-bound during compilation time. If you really want the guarantee that, "if I consume a labeled entity, the result will always be the same", then also as you pointed out, defining functions guarantees that.

Although even with functions, it is possible to declare another clause that returns a different value:

defmodule Cats do
  def foo, do: "Puff"
  def foo("not Puff"), do: "Piggles"
end

If you didn't want to declare a lot of functions, you could declare a function that returns a map of defined values.

defmodule Cats do
  def names, do: %{foo: "Puff", bar: "Piffles"}
end

Then use it like:

iex(1)> Cats.names.foo
"Puff"
iex(2)> Cats.names.bar
"Piffles"

Or multiple clauses of the same function:

defmodule Cats do
  def names(:foo), do: "Puff"
  def names(:bar), do: "Piffles"
end

iex(1)> Cats.names(:foo)
"Puff"
iex(2)> Cats.names(:bar)
"Piffles"
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74