Skip to content

Commit

Permalink
Bring behaviour section from website
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Nov 1, 2023
1 parent f5a61d1 commit 49e9472
Showing 1 changed file with 107 additions and 36 deletions.
143 changes: 107 additions & 36 deletions lib/elixir/pages/references/typespecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Type specifications (most often referred to as *typespecs*) are defined in diffe
* `@callback`
* `@macrocallback`

In addition, you can use `@typedoc` to describe a custom `@type` definition.
In addition, you can use `@typedoc` to document a custom `@type` definition.

See the "User-defined types" and "Defining a specification" sub-sections below for more information on defining types and typespecs.

Expand Down Expand Up @@ -250,61 +250,128 @@ Behaviours in Elixir (and Erlang) are a way to separate and abstract the generic

A behaviour module defines a set of functions and macros (referred to as *callbacks*) that callback modules implementing that behaviour must export. This "interface" identifies the specific part of the component. For example, the `GenServer` behaviour and functions abstract away all the message-passing (sending and receiving) and error reporting that a "server" process will likely want to implement from the specific parts such as the actions that this server process has to perform.

To define a behaviour module, it's enough to define one or more callbacks in that module. To define callbacks, the `@callback` and `@macrocallback` module attributes can be used (for function callbacks and macro callbacks respectively).
Say we want to implement a bunch of parsers, each parsing structured data: for example, a JSON parser and a MessagePack parser. Each of these two parsers will *behave* the same way: both will provide a `parse/1` function and an `extensions/0` function. The `parse/1` function will return an Elixir representation of the structured data, while the `extensions/0` function will return a list of file extensions that can be used for each type of data (e.g., `.json` for JSON files).

defmodule MyBehaviour do
@callback my_fun(arg :: any) :: any
@macrocallback my_macro(arg :: any) :: Macro.t
end
We can create a `Parser` behaviour:

```elixir
defmodule Parser do
@doc """
Parses a string.
"""
@callback parse(String.t) :: {:ok, term} | {:error, atom}

@doc """
Lists all supported file extensions.
"""
@callback extensions() :: [String.t]
end
```

As seen in the example above, defining a callback is a matter of defining a specification for that callback, made of:

* the callback name (`my_fun` or `my_macro` in the example)
* the arguments that the callback must accept (`arg :: any` in the example)
* the callback name (`parse` or `extensions` in the example)
* the arguments that the callback must accept (`String.t`)
* the *expected* type of the callback return value

### Optional callbacks
Modules adopting the `Parser` behaviour will have to implement all the functions defined with the `@callback` attribute. As you can see, `@callback` expects a function name but also a function specification like the ones used with the `@spec` attribute we saw above.

Optional callbacks are callbacks that callback modules may implement if they want to, but are not required to. Usually, behaviour modules know if they should call those callbacks based on configuration, or they check if the callbacks are defined with `function_exported?/3` or `macro_exported?/3`.
### Implementing behaviours

Optional callbacks can be defined through the `@optional_callbacks` module attribute, which has to be a keyword list with function or macro name as key and arity as value. For example:
Implementing a behaviour is straightforward:

defmodule MyBehaviour do
@callback vital_fun() :: any
@callback non_vital_fun() :: any
@macrocallback non_vital_macro(arg :: any) :: Macro.t
@optional_callbacks non_vital_fun: 0, non_vital_macro: 1
end
```elixir
defmodule JSONParser do
@behaviour Parser

One example of optional callback in Elixir's standard library is `c:GenServer.format_status/2`.
@impl Parser
def parse(str), do: {:ok, "some json " <> str} # ... parse JSON

### Implementing behaviours
@impl Parser
def extensions, do: [".json"]
end
```

To specify that a module implements a given behaviour, the `@behaviour` attribute must be used:
```elixir
defmodule CSVParser do
@behaviour Parser

defmodule MyBehaviour do
@callback my_fun(arg :: any) :: any
end
@impl Parser
def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV

defmodule MyCallbackModule do
@behaviour MyBehaviour
def my_fun(arg), do: arg
end
@impl Parser
def extensions, do: [".csv"]
end
```

If a callback module that implements a given behaviour doesn't export all the functions and macros defined by that behaviour, the user will be notified through warnings during the compilation process (no errors will happen).
If a module adopting a given behaviour doesn't implement one of the callbacks required by that behaviour, a compile-time warning will be generated.

You can also use the `@impl` attribute before a function to denote that particular function is implementation a behaviour:
Furthermore, with `@impl` you can also make sure that you are implementing the **correct** callbacks from the given behaviour in an explicit manner. For example, the following parser implements both `parse` and `extensions`. However, thanks to a typo, `BADParser` is implementing `parse/0` instead of `parse/1`.

defmodule MyCallbackModule do
@behaviour MyBehaviour
```elixir
defmodule BADParser do
@behaviour Parser

@impl true
def my_fun(arg), do: arg
end
@impl Parser
def parse, do: {:ok, "something bad"}

@impl Parser
def extensions, do: ["bad"]
end
```

This code generates a warning letting you know that you are mistakenly implementing `parse/0` instead of `parse/1`.
You can read more about `@impl` in the [module documentation](Module.html#module-impl).

### Using behaviours

Behaviours are useful because you can pass modules around as arguments and you can then *call back* to any of the functions specified in the behaviour. For example, we can have a function that receives a filename, several parsers, and parses the file based on its extension:

```elixir
@spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom}
def parse_path(filename, parsers) do
with {:ok, ext} <- parse_extension(filename),
{:ok, parser} <- find_parser(ext, parsers),
{:ok, contents} <- File.read(filename) do
parser.parse(contents)
end
end

defp parse_extension(filename) do
if ext = Path.extname(filename) do
{:ok, ext}
else
{:error, :no_extension}
end
end

defp find_parser(ext, parsers) do
if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do
{:ok, parser}
else
{:error, :no_matching_parser}
end
end
```

You could also invoke any parser directly: `CSVParser.parse(...)`.

Note you don't need to define a behaviour in order to dynamically dispatch on a module, but those features often go hand in hand.

You can also use `@impl MyBehaviour` to make clearer from which behaviour the callbacks comes from, providing even more context for future readers of your code.
### Optional callbacks

Optional callbacks are callbacks that callback modules may implement if they want to, but are not required to. Usually, behaviour modules know if they should call those callbacks based on configuration, or they check if the callbacks are defined with `function_exported?/3` or `macro_exported?/3`.

Elixir's standard library contains a few frequently used behaviours such as `GenServer`, `Supervisor`, and `Application`.
Optional callbacks can be defined through the `@optional_callbacks` module attribute, which has to be a keyword list with function or macro name as key and arity as value. For example:

defmodule MyBehaviour do
@callback vital_fun() :: any
@callback non_vital_fun() :: any
@macrocallback non_vital_macro(arg :: any) :: Macro.t
@optional_callbacks non_vital_fun: 0, non_vital_macro: 1
end

One example of optional callback in Elixir's standard library is `c:GenServer.format_status/2`.

### Inspecting behaviours

Expand All @@ -319,6 +386,10 @@ For example, for the `MyBehaviour` module defined in "Optional callbacks" above:

When using `iex`, the `IEx.Helpers.b/1` helper is also available.

## Pitfalls

There are some known pitfalls when using typespecs, they are documented next.

## The `string()` type

Elixir discourages the use of the `string()` type. The `string()` type refers to Erlang strings, which are known as "charlists" in Elixir. They do not refer to Elixir strings, which are UTF-8 encoded binaries. To avoid confusion, if you attempt to use the type `string()`, Elixir will emit a warning. You should use `charlist()`, `nonempty_charlist()`, `binary()` or `String.t()` accordingly, or any of the several literal representations for these types.
Expand Down

0 comments on commit 49e9472

Please sign in to comment.