diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 446785cfdff..dd4cf1b194b 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -9,7 +9,8 @@ ], "gitignore": true, "config": { - // Consecutive header levels (h1 -> h2 -> h3). + // Heading levels should only increment by one level at a time. + // Admonition blocks do not follow this rule. "MD001": false, // Header style. We use #s. "MD003": { diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index d4bfebc0fed..fd87af17fc7 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -9,11 +9,11 @@ This document outlines potential anti-patterns related to your code and particul ## Comments overuse -#### Problem +### Problem When you overuse comments or comment self-explanatory code, it can have the effect of making code *less readable*. -#### Example +### Example ```elixir # Returns the Unix timestamp of 5 minutes from the current time @@ -29,7 +29,7 @@ defp unix_five_min_from_now do end ``` -#### Refactoring +### Refactoring Prefer clear and self-explanatory function names, module names, and variable names when possible. In the example above, the function name explains well what the function does, so you likely won't need the comment before it. The code also explains the operations well through variable names and clear function calls. @@ -47,17 +47,17 @@ end We removed the unnecessary comments. We also added a `@five_min_in_seconds` module attribute, which serves the additional purpose of giving a name to the "magic" number `60 * 5`, making the code clearer and more expressive. -#### Additional remarks +### Additional remarks Elixir makes a clear distinction between **documentation** and code comments. The language has built-in first-class support for documentation through `@doc`, `@moduledoc`, and more. See the ["Writing documentation"](../getting-started/writing-documentation.md) guide for more information. ## Complex `else` clauses in `with` -#### Problem +### Problem This anti-pattern refers to `with` expressions that flatten all its error clauses into a single complex `else` block. This situation is harmful to the code readability and maintainability because it's difficult to know from which clause the error value came. -#### Example +### Example An example of this anti-pattern, as shown below, is a function `open_decoded_file/1` that reads a Base64-encoded string content from a file and returns a decoded binary string. This function uses a `with` expression that needs to handle two possible errors, all of which are concentrated in a single complex `else` block. @@ -75,7 +75,7 @@ end In the code above, it is unclear how each pattern on the left side of `<-` relates to their error at the end. The more patterns in a `with`, the less clear the code gets, and the more likely it is that unrelated failures will overlap each other. -#### Refactoring +### Refactoring In this situation, instead of concentrating all error handling within a single complex `else` block, it is better to normalize the return types in specific private functions. In this way, `with` can focus on the success case and the errors are normalized closer to where they happen, leading to better organized and maintainable code. @@ -104,11 +104,11 @@ end ## Complex extractions in clauses -#### Problem +### Problem When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent an anti-pattern, but when you have *extractions made across several clauses and several arguments of the same function*, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This anti-pattern is related to [Unrelated multi-clause function](design-anti-patterns.md#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way. -#### Example +### Example The multi-clause function `drive/1` is extracting fields of an `%User{}` struct for usage in the clause expression (`age`) and for usage in the function body (`name`): @@ -124,7 +124,7 @@ end While the example above is small and does not constitute an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not. -#### Refactoring +### Refactoring As shown below, a possible solution to this anti-pattern is to extract only pattern/guard related variables in the signature once you have many arguments or multiple clauses: @@ -142,13 +142,13 @@ end ## Dynamic atom creation -#### Problem +### Problem An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself. However, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1 048 576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation. For these reasons, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible. -#### Example +### Example Picture yourself implementing code that converts string values into atoms. These strings could have been received from an external system, either as part of a request into our application, or as part of a response to your application. This dynamic and unpredictable scenario poses a security risk, as these uncontrolled conversions can potentially trigger out-of-memory errors. @@ -167,7 +167,7 @@ iex> MyRequestHandler.parse(%{"status" => "ok", "message" => "all good"}) When we use the `String.to_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system. -#### Refactoring +### Refactoring To eliminate this anti-pattern, developers must either perform explicit conversions by mapping strings to atoms or replace the use of `String.to_atom/1` with `String.to_existing_atom/1`. An explicit conversion could be done as follows: @@ -237,11 +237,11 @@ However, keep in mind using a module attribute or defining the atoms in the modu ## Long parameter list -#### Problem +### Problem In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point where the function's interface becomes confusing and prone to errors during use. -#### Example +### Example In the following example, the `loan/6` functions takes too many arguments, causing its interface to be confusing and potentially leading developers to introduce errors during calls to this function. @@ -254,7 +254,7 @@ defmodule Library do end ``` -#### Refactoring +### Refactoring To address this anti-pattern, related arguments can be grouped using key-value data structures, such as maps, structs, or even keyword lists in the case of optional arguments. This effectively reduces the number of arguments and the key-value data structures adds clarity to the caller. @@ -274,13 +274,13 @@ Other times, a function may legitimately take half a dozen or more completely un ## Namespace trespassing -#### Problem +### Problem This anti-pattern manifests when a package author or a library defines modules outside of its "namespace". A library should use its name as a "prefix" for all of its modules. For example, a package named `:my_lib` should define all of its modules within the `MyLib` namespace, such as `MyLib.User`, `MyLib.SubModule`, `MyLib.Application`, and `MyLib` itself. This is important because the Erlang VM can only load one instance of a module at a time. So if there are multiple libraries that define the same module, then they are incompatible with each other due to this limitation. By always using the library name as a prefix, it avoids module name clashes due to the unique prefix. -#### Example +### Example This problem commonly manifests when writing an extension of another library. For example, imagine you are writing a package that adds authentication to [Plug](https://github.com/elixir-plug/plug) called `:plug_auth`. You must avoid defining modules within the `Plug` namespace: @@ -292,7 +292,7 @@ end Even if `Plug` does not currently define a `Plug.Auth` module, it may add such a module in the future, which would ultimately conflict with `plug_auth`'s definition. -#### Refactoring +### Refactoring Given the package is named `:plug_auth`, it must define modules inside the `PlugAuth` namespace: @@ -314,7 +314,7 @@ There are few known exceptions to this anti-pattern: ## Non-assertive map access -#### Problem +### Problem In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. @@ -324,14 +324,14 @@ When a key is optional, the `map[:key]` notation must be used instead. This way, When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpectedly missing, you will have a `nil` value propagate through the system, instead of raising on map access. -##### Table: Comparison of map access notations +#### Table: Comparison of map access notations | Access notation | Key exists | Key doesn't exist | Use case | | --------------- | ---------- | ----------------- | -------- | | `map.key` | Returns the value | Raises `KeyError` | Structs and maps with known atom keys | | `map[:key]` | Returns the value | Returns `nil` | Any `Access`-based data structure, optional keys | -#### Example +### Example The function `plot/1` tries to draw a graphic to represent the position of a point in a Cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D Cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys: @@ -378,7 +378,7 @@ iex> distance_from_origin = :math.sqrt(x * x + y * y) The error above occurs later in the code because `nil` (from missing `:x`) is invalid for arithmetic operations, making it harder to identify the original issue. -#### Refactoring +### Refactoring To remove this anti-pattern, we must use the dynamic `map[:key]` syntax and the static `map.key` notation according to our requirements. We expect `:x` and `:y` to always exist, but not `:z`. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: @@ -464,11 +464,11 @@ This anti-pattern was formerly known as [Accessing non-existent map/struct field ## Non-assertive pattern matching -#### Problem +### Problem Overall, Elixir systems are composed of many supervised processes, so the effects of an error are localized to a single process, and don't propagate to the entire application. A supervisor detects the failing process, reports it, and possibly restarts it. This anti-pattern arises when developers write defensive or imprecise code, capable of returning incorrect values which were not planned for, instead of programming in an assertive style through pattern matching and guards. -#### Example +### Example The function `get_value/2` tries to extract a value from a specific key of a URL query string. As it is not implemented using pattern matching, `get_value/2` always returns a value, regardless of the format of the URL query string passed as a parameter in the call. Sometimes the returned value will be valid. However, if a URL query string with an unexpected format is used in the call, `get_value/2` will extract incorrect values from it: @@ -496,7 +496,7 @@ iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "univ "institution" # <= why not "institution=UFMG"? or only "UFMG"? ``` -#### Refactoring +### Refactoring To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behavior, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted: @@ -554,11 +554,11 @@ This anti-pattern was formerly known as [Speculative assumptions](https://github ## Non-assertive truthiness -#### Problem +### Problem Elixir provides the concept of truthiness: `nil` and `false` are considered "falsy" and all other values are "truthy". Many constructs in the language, such as `&&/2`, `||/2`, and `!/1` handle truthy and falsy values. Using those operators is not an anti-pattern. However, using those operators when all operands are expected to be booleans, may be an anti-pattern. -#### Example +### Example The simplest scenario where this anti-pattern manifests is in conditionals, such as: @@ -572,7 +572,7 @@ end Given both operands of `&&/2` are booleans, the code is more generic than necessary, and potentially unclear. -#### Refactoring +### Refactoring To remove this anti-pattern, we can replace `&&/2`, `||/2`, and `!/1` by `and/2`, `or/2`, and `not/1` respectively. These operators assert at least their first argument is a boolean: @@ -588,11 +588,11 @@ This technique may be particularly important when working with Erlang code. Erla ## Structs with 32 fields or more -#### Problem +### Problem Structs in Elixir are implemented as compile-time maps, which have a predefined amount of fields. When structs have 32 or more fields, their internal representation in the Erlang Virtual Machines changes, potentially leading to bloating and higher memory usage. -#### Example +### Example Any struct with 32 or more fields will be problematic: @@ -623,7 +623,7 @@ end All user structs will point to the same tuple keys at compile-time, also reducing the memory cost of instantiating structs with `%MyStruct{...}` notation. This optimization is also not available if the struct has 32 keys or more. -#### Refactoring +### Refactoring Removing this anti-pattern, in a nutshell, requires ensuring your struct has fewer than 32 fields. There are a few techniques you could apply: diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 6e0d6dfa30e..460c2b4c9be 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -9,11 +9,11 @@ This document outlines potential anti-patterns related to your modules, function ## Alternative return types -#### Problem +### Problem This anti-pattern refers to functions that receive options (typically as a *keyword list* parameter) that drastically change their return type. Because options are optional and sometimes set dynamically, if they also change the return type, it may be hard to understand what the function actually returns. -#### Example +### Example An example of this anti-pattern, as shown below, is when a function has many alternative return types, depending on the options received as a parameter. @@ -42,7 +42,7 @@ iex> AlternativeInteger.parse("13", discard_rest: true) 13 ``` -#### Refactoring +### Refactoring To refactor this anti-pattern, as shown next, add a specific function for each return type (for example, `parse_discard_rest/1`), no longer delegating this to options passed as arguments. @@ -72,13 +72,13 @@ iex> AlternativeInteger.parse_discard_rest("13") ## Boolean obsession -#### Problem +### Problem This anti-pattern happens when booleans are used instead of atoms to encode information. The usage of booleans themselves is not an anti-pattern, but whenever multiple booleans are used with overlapping states, replacing the booleans by atoms (or composite data types such as *tuples*) may lead to clearer code. This is a special case of [*Primitive obsession*](#primitive-obsession), specific to boolean values. -#### Example +### Example An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behavior in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related. @@ -94,7 +94,7 @@ defmodule MyApp do end ``` -#### Refactoring +### Refactoring Instead of using multiple options, the code above could be refactored to receive a single option, called `:role`, that can be either `:admin`, `:editor`, or `:default`: @@ -128,11 +128,11 @@ Remember booleans are internally represented as atoms. Therefore there is no per ## Exceptions for control-flow -#### Problem +### Problem This anti-pattern refers to code that uses `Exception`s for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern. -#### Example +### Example An example of this anti-pattern, as shown below, is using `try/rescue` to deal with file operations: @@ -157,7 +157,7 @@ could not read file "invalid_file": no such file or directory :ok ``` -#### Refactoring +### Refactoring To refactor this anti-pattern, as shown next, use `File.read/1`, which returns tuples instead of raising when a file cannot be read: @@ -204,11 +204,11 @@ This anti-pattern was formerly known as [Using exceptions for control-flow](http ## Primitive obsession -#### Problem +### Problem This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are excessively used to carry structured information, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain. -#### Example +### Example An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value. @@ -228,7 +228,7 @@ While you may receive the `address` as a string from a database, web request, or Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/). -#### Refactoring +### Refactoring Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remaining functions. With this modification, we can extract each field of this composite type individually when needed. @@ -256,11 +256,11 @@ end ## Unrelated multi-clause function -#### Problem +### Problem Using multi-clause functions is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which is an anti-pattern. -#### Example +### Example A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behavior of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. @@ -285,7 +285,7 @@ end If updating an animal is completely different from updating a product and requires a different set of rules, it may be worth splitting those over different functions or even different modules. -#### Refactoring +### Refactoring As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's callers, so be careful! @@ -348,11 +348,11 @@ The difference here is that the `struct/2` function behaves precisely the same f ## Using application configuration for libraries -#### Problem +### Problem The [*application environment*](https://hexdocs.pm/elixir/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways. -#### Example +### Example The `DashSplitter` module represents a library that configures the behavior of its functions through the global application environment. These configurations are concentrated in the *config/config.exs* file, shown below: @@ -385,7 +385,7 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") ["Lucas", "Francisco", "da-Matta-Vegi"] ``` -#### Refactoring +### Refactoring To remove this anti-pattern, this type of configuration should be performed using a parameter passed to the function. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application: diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index 9ccb413642e..a9d390c1ea0 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -9,13 +9,13 @@ This document outlines potential anti-patterns related to meta-programming. ## Compile-time dependencies -#### Problem +### Problem This anti-pattern is related to dependencies between files in Elixir. Because macros are used at compile-time, the use of any macro in Elixir adds a compile-time dependency to the module that defines the macro. However, when macros are used in the body of a module, the arguments to the macro themselves may become compile-time dependencies. These dependencies may lead to dependency graphs where changing a single file causes several files to be recompiled. -#### Example +### Example Let's take the [`Plug`](https://github.com/elixir-plug/plug) library as an example. The `Plug` project allows you to specify several modules, also known as plugs, which will be invoked whenever there is a request. As a user of `Plug`, you would use it as follows: @@ -58,7 +58,7 @@ end The trouble with the code above is that, because the `plug MyApp.Authentication` was invoked at compile-time, the module `MyApp.Authentication` is now a compile-time dependency of `MyApp`, even though `MyApp.Authentication` is never used at compile-time. If `MyApp.Authentication` depends on other modules, even at runtime, this can now lead to a large recompilation graph in case of changes. -#### Refactoring +### Refactoring To address this anti-pattern, a macro can expand literals within the context they are meant to be used, as follows: @@ -80,11 +80,11 @@ In actual projects, developers may use `mix xref trace path/to/file.ex` to execu ## Large code generation -#### Problem +### Problem This anti-pattern is related to macros that generate too much code. When a macro generates a large amount of code, it impacts how the compiler and/or the runtime work. The reason for this is that Elixir may have to expand, compile, and execute the code multiple times, which will make compilation slower and the resulting compiled artifacts larger. -#### Example +### Example Imagine you are defining a router for a web application, where you could have macros like `get/2`. On every invocation of the macro (which could be hundreds), the code inside `get/2` will be expanded and compiled, which can generate a large volume of code overall. @@ -109,7 +109,7 @@ defmodule Routes do end ``` -#### Refactoring +### Refactoring To remove this anti-pattern, the developer should simplify the macro, delegating part of its work to other functions. As shown below, by encapsulating the code inside `quote/1` inside the function `__define__/3` instead, we reduce the code that is expanded and compiled on every invocation of the macro, and instead we dispatch to a function to do the bulk of the work. @@ -137,11 +137,11 @@ end ## Unnecessary macros -#### Problem +### Problem *Macros* are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. -#### Example +### Example The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear: @@ -164,7 +164,7 @@ iex> MyMath.sum(3 + 1, 5 + 6) 15 ``` -#### Refactoring +### Refactoring To remove this anti-pattern, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require/2` call is no longer needed: @@ -185,13 +185,13 @@ iex> MyMath.sum(3+1, 5+6) ## `use` instead of `import` -#### Problem +### Problem Elixir has mechanisms such as `import/1`, `alias/1`, and `use/1` to establish dependencies between modules. Code implemented with these mechanisms does not characterize a smell by itself. However, while the `import/1` and `alias/1` directives have lexical scope and only facilitate a module calling functions of another, the `use/1` directive has a *broader scope*, which can be problematic. The `use/1` directive allows a module to inject any type of code into another, including propagating dependencies. In this way, using the `use/1` directive makes code harder to read, because to understand exactly what will happen when it references a module, it is necessary to have knowledge of the internal details of the referenced module. -#### Example +### Example The code shown below is an example of this anti-pattern. It defines three modules -- `ModuleA`, `Library`, and `ClientApp`. `ClientApp` is reusing code from the `Library` via the `use/1` directive, but is unaware of its internal details. This makes it harder for the author of `ClientApp` to visualize which modules and functionality are now available within its module. To make matters worse, `Library` also imports `ModuleA`, which defines a `foo/0` function that conflicts with a local function defined in `ClientApp`: @@ -239,7 +239,7 @@ error: imported ModuleA.foo/0 conflicts with local function └ client_app.ex:4: ``` -#### Refactoring +### Refactoring To remove this anti-pattern, we recommend library authors avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists: @@ -290,11 +290,11 @@ For convenience, the markup notation to generate the admonition block above is t ## Untracked compile-time dependencies -#### Problem +### Problem This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler unable to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro. -#### Example +### Example For example, imagine you invoke a module at compile-time, you could write it as such: @@ -346,7 +346,7 @@ defmodule MyModule do end ``` -#### Refactoring +### Refactoring To address this anti-pattern, you should avoid defining module names programmatically. For example, if you need to dispatch to multiple modules, do so by using full module names. diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 07e27eb268b..3e97a4fb0be 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -9,11 +9,11 @@ This document outlines potential anti-patterns related to processes and process- ## Code organization by process -#### Problem +### Problem This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system. -#### Example +### Example An example of this anti-pattern, as shown below, is a module that implements arithmetic operations (like `add` and `subtract`) by means of a `GenServer` process. If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck. @@ -85,11 +85,11 @@ iex> Calculator.subtract(2, 3) ## Scattered process interfaces -#### Problem +### Problem In Elixir, the use of an `Agent`, a `GenServer`, or any other process abstraction is not an anti-pattern in itself. However, when the responsibility for direct interaction with a process is spread throughout the entire system, it can become problematic. This bad practice can increase the difficulty of code maintenance and make the code more prone to bugs. -#### Example +### Example The following code seeks to illustrate this anti-pattern. The responsibility for interacting directly with the `Agent` is spread across four different modules (`A`, `B`, `C`, and `D`). @@ -195,13 +195,13 @@ This anti-pattern was formerly known as [Agent obsession](https://github.com/luc ## Sending unnecessary data -#### Problem +### Problem Sending a message to a process can be an expensive operation if the message is big enough. That's because that message will be fully copied to the receiving process, which may be CPU and memory intensive. This is due to Erlang's "share nothing" architecture, where each process has its own memory, which simplifies and speeds up garbage collection. This is more obvious when using `send/2`, `GenServer.call/3`, or the initial data in `GenServer.start_link/3`. Notably this also happens when using `spawn/1`, `Task.async/1`, `Task.async_stream/3`, and so on. It is more subtle here as the anonymous function passed to these functions captures the variables it references, and all captured variables will be copied over. By doing this, you can accidentally send way more data to a process than you actually need. -#### Example +### Example Imagine you were to implement some simple reporting of IP addresses that made requests against your application. You want to do this asynchronously and not block processing, so you decide to use `spawn/1`. It may seem like a good idea to hand over the whole connection because we might need more data later. However passing the connection results in copying a lot of unnecessary data like the request body, params, etc. @@ -249,11 +249,11 @@ GenServer.cast(pid, {:report_ip_address, conn.remote_ip}) ## Unsupervised processes -#### Problem +### Problem In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their lifecycle. -#### Example +### Example The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `Agent` process *outside a supervision tree*. diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index fbb32c65f9f..4a7a2ca4ae4 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -155,7 +155,7 @@ $ iex iex> :observer.start() ``` -> #### Missing dependencies {: .warning} +> ### Missing dependencies {: .warning} > > When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before: > diff --git a/lib/elixir/pages/getting-started/io-and-the-file-system.md b/lib/elixir/pages/getting-started/io-and-the-file-system.md index 3ffff5d8375..3364f746f11 100644 --- a/lib/elixir/pages/getting-started/io-and-the-file-system.md +++ b/lib/elixir/pages/getting-started/io-and-the-file-system.md @@ -32,7 +32,7 @@ hello world The `File` module contains functions that allow us to open files as IO devices. By default, files are opened in binary mode, which requires developers to use the specific `IO.binread/2` and `IO.binwrite/2` functions from the `IO` module: -> #### Potential data loss warning {: .warning} +> ### Potential data loss warning {: .warning} > > The following code opens a file for writing. If an existing file is available at the given path, its contents will be deleted. diff --git a/lib/elixir/pages/getting-started/lists-and-tuples.md b/lib/elixir/pages/getting-started/lists-and-tuples.md index 9a7e06a124b..0ef086ebcc2 100644 --- a/lib/elixir/pages/getting-started/lists-and-tuples.md +++ b/lib/elixir/pages/getting-started/lists-and-tuples.md @@ -75,7 +75,7 @@ Implemented protocols We will talk more about charlists in the ["Binaries, strings, and charlists"](binaries-strings-and-charlists.md) chapter. -> #### Single-quoted strings {: .info} +> ### Single-quoted strings {: .info} > > In Elixir, you can also use `'hello'` to build charlists, but this notation has been soft-deprecated in Elixir v1.15 and will emit warnings in future versions. Prefer to write `~c"hello"` instead. diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 32ff5ad12eb..69b6c0e1c50 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -6,7 +6,7 @@ # Typespecs reference -> #### Typespecs are not set-theoretic types {: .warning} +> ### Typespecs are not set-theoretic types {: .warning} > > Elixir is in the process of implementing its > [own type system](./gradual-set-theoretic-types.md) based on set-theoretic types. @@ -59,7 +59,7 @@ The syntax Elixir provides for type specifications is similar to [the one in Erl The notation to represent the union of types is the pipe `|`. For example, the typespec `type :: atom() | pid() | tuple()` creates a type `type` that can be either an `atom`, a `pid`, or a `tuple`. This is usually called a [sum type](https://en.wikipedia.org/wiki/Tagged_union) in other languages -> #### Differences with set-theoretic types {: .warning} +> ### Differences with set-theoretic types {: .warning} > > While they do share some similarities, the types below do not map one-to-one > to the new types from the set-theoretic type system.