Skip to content

Commit 813c5aa

Browse files
committed
Update docs for module attributes
It is not necessary to push module attributes as a mechanism for constants. In fact, regular functions are a better default for constants in the majority of the cases.
1 parent c5816a2 commit 813c5aa

File tree

2 files changed

+40
-64
lines changed

2 files changed

+40
-64
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3547,37 +3547,6 @@ defmodule Kernel do
35473547
def example1, do: files()[:example1]
35483548
def example2, do: files()[:example2]
35493549
3550-
## Attention! Compile-time dependencies
3551-
3552-
Keep in mind references to other modules, even in module attributes,
3553-
generate compile-time dependencies to said modules.
3554-
3555-
For example, take this common pattern:
3556-
3557-
@values [:foo, :bar, :baz]
3558-
3559-
def handle_arg(arg) when arg in @values do
3560-
...
3561-
end
3562-
3563-
While the above is fine, imagine if instead you have actual
3564-
module names in the module attribute, like this:
3565-
3566-
@values [Foo, Bar, Baz]
3567-
3568-
def handle_arg(arg) when arg in @values do
3569-
...
3570-
end
3571-
3572-
The code above will define a compile-time dependency on the modules
3573-
`Foo`, `Bar`, and `Baz`, in a way that, if any of them change, the
3574-
current module will have to recompile. In such cases, it may be
3575-
preferred to avoid the module attribute altogether:
3576-
3577-
def handle_arg(arg) when arg in [Foo, Bar, Baz] do
3578-
...
3579-
end
3580-
35813550
"""
35823551
defmacro @expr
35833552

lib/elixir/pages/getting-started/module-attributes.md

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Module attributes
22

3-
Module attributes in Elixir serve three purposes:
3+
Module attributes in Elixir serve two purposes:
44

5-
1. They serve to annotate the module, often with information to be used by the user or the VM.
6-
2. They work as constants.
7-
3. They work as a temporary module storage to be used during compilation.
5+
1. They serve to annotate the module and its functions.
6+
2. They work as a temporary module storage to be used during compilation.
87

9-
Let's check each case, one by one.
8+
Let's check these examples.
109

1110
## As annotations
1211

@@ -64,11 +63,11 @@ iex> h Math.sum # Access the docs for the sum function
6463

6564
We also provide a tool called [ExDoc](https://github.com/elixir-lang/ex_doc) which is used to generate HTML pages from the documentation.
6665

67-
You can take a look at the docs for `Module` for a complete list of supported attributes. Elixir also uses attributes to define [typespecs](../references/typespecs.md), which can be used to declare contracts between modules later.
66+
You can take a look at the docs for `Module` for a complete list of supported attributes. Elixir also uses attributes to annotate our code with [typespecs](../references/typespecs.md).
6867

69-
## As "constants"
68+
## As temporary storage
7069

71-
Elixir developers often use module attributes when they wish to make a value more visible or reusable:
70+
So far, we have seen how to define attributes, but how can read them? Let's see an example:
7271

7372
```elixir
7473
defmodule MyServer do
@@ -96,8 +95,8 @@ defmodule MyServer do
9695
def second_data, do: @my_data
9796
end
9897

99-
MyServer.first_data #=> 14
100-
MyServer.second_data #=> 13
98+
MyServer.first_data() #=> 14
99+
MyServer.second_data() #=> 13
101100
```
102101

103102
> Do not add a newline between the attribute and its value, otherwise Elixir will assume you are reading the value, rather than setting it.
@@ -128,9 +127,9 @@ defmodule MyApp.Status do
128127
end
129128
```
130129

131-
This can be useful for pre-computing constant values, but it can also cause problems if you're expecting the function to be called at runtime. For example, if you are reading a value from a database or an environment variable inside an attribute, be aware that it will read that value only at compilation time. However, note you cannot invoke functions defined in the same module as part of the attribute itself, as those functions have not yet been defined.
130+
This can be useful for pre-computing values and then injecting its results into the module. This is what we mean by temporary storage: after the module is compiled, the module attribute is discarded, except for the functions that have read the attribute. Note you cannot invoke functions defined in the same module as part of the attribute itself, as those functions have not yet been defined.
132131

133-
Every time an attribute is read inside a function, Elixir takes a snapshot of its current value. Therefore if you read the same attribute multiple times inside multiple functions, you may end-up making multiple copies of it. That's usually not an issue, but if you are using functions to compute large module attributes, that can slow down compilation. The solution is to move the attribute to shared function. For example, instead of this:
132+
Every time an attribute is read inside a function, Elixir takes a snapshot of its current value. Therefore if you read the same attribute multiple times inside multiple functions, you may end-up making multiple copies of it. Generally speaking, you want to avoid reading the same attribute multiple times and instead move it to shared function. For example, instead of this:
134133

135134
```elixir
136135
def some_function, do: do_something_with(@example)
@@ -145,25 +144,33 @@ def another_function, do: do_something_else_with(example())
145144
defp example, do: @example
146145
```
147146

148-
If `@example` is cheap to compute, it may be even better to skip the module attribute altogether, and compute its value inside the function.
149-
150-
### Accumulating attributes
151-
152-
Normally, repeating a module attribute will cause its value to be reassigned, but there are circumstances where you may want to [configure the module attribute](`Module.register_attribute/3`) so that its values are accumulated:
153-
154-
```elixir
155-
defmodule Foo do
156-
Module.register_attribute(__MODULE__, :param, accumulate: true)
157-
158-
@param :foo
159-
@param :bar
160-
# here @param == [:bar, :foo]
161-
end
162-
```
163-
164-
## As temporary storage
165-
166-
To see an example of using module attributes as storage, look no further than Elixir's unit test framework called `ExUnit`. ExUnit uses module attributes for multiple different purposes:
147+
> #### Attributes as constants {: .warning}
148+
>
149+
> Sometimes developers want to use module attributes as constants, however, functions themselves play a better role. For example, instead of defining:
150+
>
151+
> ```elixir
152+
> @hours_in_a_day 24
153+
> ```
154+
>
155+
> Instead, you should prefer:
156+
>
157+
> ```elixir
158+
> defp hours_in_a_day(), do: 24
159+
> ```
160+
>
161+
> Or a public function if it needs to be shared between modules.
162+
>
163+
> This also holds true even for complex data structures. For example, if you need to define some configuration, you can define it as:
164+
>
165+
> ```elixir
166+
> defp system_config(), do: %{timezone: "Etc/UTC", locale: "pt-BR"}
167+
> ```
168+
>
169+
> Given data structures in Elixir are immutable, only a single instance of the data structure above is allocated, and shared across all functions calls, since it doesn't depend on any expression.
170+
171+
## Going further
172+
173+
Libraries and frameworks can leverage module attributes to provide custom annotations. To see an example, look no further than Elixir's unit test framework called `ExUnit`. ExUnit uses module attributes for multiple different purposes:
167174
168175
```elixir
169176
defmodule MyTest do
@@ -177,8 +184,8 @@ defmodule MyTest do
177184
end
178185
```
179186
180-
In the example above, `ExUnit` stores the value of `async: true` in a module attribute to change how the module is compiled. Tags are also defined as `accumulate: true` attributes, and they store tags that can be used to setup and filter tests. For example, you can avoid running external tests on your machine because they are slow and dependent on other services, while they can still be enabled in your build system.
187+
In the example above, `ExUnit` stores the value of `async: true` in a module attribute to change how the module is compiled. Tags also work as annotations and they can be supplied multiple times, thanks to Elixir's ability to [accumulate attribute](`Module.register_attribute/3`). Then yuou can use tags to setup and filter tests, such as avoiding executing Unix specific tests while running your test suite on Windows.
181188

182-
In order to understand the underlying code, we'd need macros, so we will revisit this pattern in the meta-programming guide and learn how to use module attributes as storage to allow developers to create Domain Specific Languages (DSLs).
189+
To fully understand how ExUnit works, we'd need macros, so we will revisit this pattern in the Meta-programming guide and learn how to use module attributes as storage for custom annotations.
183190

184191
In the next chapters, we'll explore structs and protocols before moving to exception handling and other constructs like sigils and comprehensions.

0 commit comments

Comments
 (0)