summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@plataformatec.com.br>2019-02-13 10:52:47 +0100
committerJosé Valim <jose.valim@plataformatec.com.br>2019-02-13 10:52:47 +0100
commitbf3f4b5d6d023e843beb9c73ed894a06a5ebed3d (patch)
tree702ecbce51a058f90079edb8f2a2e9db872bf7a7
parent6e7e9a994a967698d1645eb01e3635728347da94 (diff)
downloadelixir-bf3f4b5d6d023e843beb9c73ed894a06a5ebed3d.tar.gz
Consolidate Protocols information in the protocol module
-rw-r--r--lib/elixir/lib/kernel.ex232
-rw-r--r--lib/elixir/lib/protocol.ex240
2 files changed, 235 insertions, 237 deletions
diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex
index 69732d761..c9cf52de9 100644
--- a/lib/elixir/lib/kernel.ex
+++ b/lib/elixir/lib/kernel.ex
@@ -4380,232 +4380,7 @@ defmodule Kernel do
@doc ~S"""
Defines a protocol.
- A protocol specifies an API that should be defined by its
- implementations.
-
- ## Examples
-
- In Elixir, we have two verbs for checking how many items there
- are in a data structure: `length` and `size`. `length` means the
- information must be computed. For example, `length(list)` needs to
- traverse the whole list to calculate its length. On the other hand,
- `tuple_size(tuple)` and `byte_size(binary)` do not depend on the
- tuple and binary size as the size information is precomputed in
- the data structure.
-
- Although Elixir includes specific functions such as `tuple_size`,
- `binary_size` and `map_size`, sometimes we want to be able to
- retrieve the size of a data structure regardless of its type.
- In Elixir we can write polymorphic code, i.e. code that works
- with different shapes/types, by using protocols. A size protocol
- could be implemented as follows:
-
- defprotocol Size do
- @doc "Calculates the size (and not the length!) of a data structure"
- def size(data)
- end
-
- Now that the protocol can be implemented for every data structure
- the protocol may have a compliant implementation for:
-
- defimpl Size, for: BitString do
- def size(binary), do: byte_size(binary)
- end
-
- defimpl Size, for: Map do
- def size(map), do: map_size(map)
- end
-
- defimpl Size, for: Tuple do
- def size(tuple), do: tuple_size(tuple)
- end
-
- Notice we didn't implement it for lists as we don't have the
- `size` information on lists, rather its value needs to be
- computed with `length`.
-
- It is possible to implement protocols for all Elixir types:
-
- * Structs (see below)
- * `Tuple`
- * `Atom`
- * `List`
- * `BitString`
- * `Integer`
- * `Float`
- * `Function`
- * `PID`
- * `Map`
- * `Port`
- * `Reference`
- * `Any` (see below)
-
- ## Protocols and Structs
-
- The real benefit of protocols comes when mixed with structs.
- For instance, Elixir ships with many data types implemented as
- structs, like `MapSet`. We can implement the `Size` protocol
- for those types as well:
-
- defimpl Size, for: MapSet do
- def size(map_set), do: MapSet.size(map_set)
- end
-
- When implementing a protocol for a struct, the `:for` option can
- be omitted if the `defimpl` call is inside the module that defines
- the struct:
-
- defmodule User do
- defstruct [:email, :name]
-
- defimpl Size do
- # two fields
- def size(%User{}), do: 2
- end
- end
-
- If a protocol implementation is not found for a given type,
- invoking the protocol will raise unless it is configured to
- fall back to `Any`. Conveniences for building implementations
- on top of existing ones are also available, look at `defstruct/1`
- for more information about deriving
- protocols.
-
- ## Fallback to `Any`
-
- In some cases, it may be convenient to provide a default
- implementation for all types. This can be achieved by setting
- the `@fallback_to_any` attribute to `true` in the protocol
- definition:
-
- defprotocol Size do
- @fallback_to_any true
- def size(data)
- end
-
- The `Size` protocol can now be implemented for `Any`:
-
- defimpl Size, for: Any do
- def size(_), do: 0
- end
-
- Although the implementation above is arguably not a reasonable
- one. For example, it makes no sense to say a PID or an integer
- have a size of `0`. That's one of the reasons why `@fallback_to_any`
- is an opt-in behaviour. For the majority of protocols, raising
- an error when a protocol is not implemented is the proper behaviour.
-
- ## Multiple implementations
-
- Protocols can also be implemented for multiple types at once:
-
- defprotocol Reversible do
- def reverse(term)
- end
-
- defimpl Reversible, for: [Map, List] do
- def reverse(term), do: Enum.reverse(term)
- end
-
- ## Types
-
- Defining a protocol automatically defines a type named `t`, which
- can be used as follows:
-
- @spec print_size(Size.t()) :: :ok
- def print_size(data) do
- result =
- case Size.size(data) do
- 0 -> "data has no items"
- 1 -> "data has one item"
- n -> "data has #{n} items"
- end
-
- IO.puts(result)
- end
-
- The `@spec` above expresses that all types allowed to implement the
- given protocol are valid argument types for the given function.
-
- ## Reflection
-
- Any protocol module contains three extra functions:
-
- * `__protocol__/1` - returns the protocol information. The function takes
- one of the following atoms:
-
- * `:consolidated?` - returns whether the protocol is consolidated
-
- * `:functions` - returns keyword list of protocol functions and their arities
-
- * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
- implementing the protocol, otherwise `:not_consolidated`
-
- * `:module` - the protocol module atom name
-
- * `impl_for/1` - receives a structure and returns the module that
- implements the protocol for the structure, `nil` otherwise
-
- * `impl_for!/1` - same as above but raises an error if an implementation is
- not found
-
- For example, for the `Enumerable` protocol we have:
-
- iex> Enumerable.__protocol__(:functions)
- [count: 1, member?: 2, reduce: 3, slice: 1]
-
- iex> Enumerable.impl_for([])
- Enumerable.List
-
- iex> Enumerable.impl_for(42)
- nil
-
- ## Consolidation
-
- In order to cope with code loading in development, protocols in
- Elixir provide a slow implementation of protocol dispatching specific
- to development.
-
- In order to speed up dispatching in production environments, where
- all implementations are known up-front, Elixir provides a feature
- called protocol consolidation. Consolidation directly links protocols
- to their implementations in a way that invoking a function from a
- consolidated protocol is equivalent to invoking two remote functions.
-
- Protocol consolidation is applied by default to all Mix projects during
- compilation. This may be an issue during test. For instance, if you want
- to implement a protocol during test, the implementation will have no
- effect, as the protocol has already been consolidated. One possible
- solution is to include compilation directories that are specific to your
- test environment in your mix.exs:
-
- def project do
- ...
- elixirc_paths: elixirc_paths(Mix.env())
- ...
- end
-
- defp elixirc_paths(:test), do: ["lib", "test/support"]
- defp elixirc_paths(_), do: ["lib"]
-
- And then you can define the implementations specific to the test environment
- inside `test/support/some_file.ex`.
-
- Another approach is to disable protocol consolidation during tests in your
- mix.exs:
-
- def project do
- ...
- consolidate_protocols: Mix.env() != :test
- ...
- end
-
- Although doing so is not recommended as it may affect your test suite
- performance.
-
- Finally note all protocols are compiled with `debug_info` set to `true`,
- regardless of the option set by `elixirc` compiler. The debug info is
- used for consolidation and it may be removed after consolidation.
+ See the `Protocol` module for more information.
"""
defmacro defprotocol(name, do_block)
@@ -4616,10 +4391,7 @@ defmodule Kernel do
@doc """
Defines an implementation for the given protocol.
- See `defprotocol/2` for more information and examples on protocols.
-
- Inside an implementation, the name of the protocol can be accessed
- via `@protocol` and the current target as `@for`.
+ See the `Protocol` module for more information.
"""
defmacro defimpl(name, opts, do_block \\ []) do
merged = Keyword.merge(opts, do_block)
diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex
index 90fdd2363..12bc0b24f 100644
--- a/lib/elixir/lib/protocol.ex
+++ b/lib/elixir/lib/protocol.ex
@@ -1,15 +1,241 @@
defmodule Protocol do
@moduledoc """
- Functions for working with protocols.
- """
+ Reference and functions for working with protocols.
- @doc """
- Defines a new protocol function.
+ A protocol specifies an API that should be defined by its
+ implementations. A protocol is defined with `Kernel.defprotocol/2`
+ and its implementations with `Kernel.defimpl/2`.
+
+ ## Examples
+
+ In Elixir, we have two verbs for checking how many items there
+ are in a data structure: `length` and `size`. `length` means the
+ information must be computed. For example, `length(list)` needs to
+ traverse the whole list to calculate its length. On the other hand,
+ `tuple_size(tuple)` and `byte_size(binary)` do not depend on the
+ tuple and binary size as the size information is precomputed in
+ the data structure.
+
+ Although Elixir includes specific functions such as `tuple_size`,
+ `binary_size` and `map_size`, sometimes we want to be able to
+ retrieve the size of a data structure regardless of its type.
+ In Elixir we can write polymorphic code, i.e. code that works
+ with different shapes/types, by using protocols. A size protocol
+ could be implemented as follows:
+
+ defprotocol Size do
+ @doc "Calculates the size (and not the length!) of a data structure"
+ def size(data)
+ end
+
+ Now that the protocol can be implemented for every data structure
+ the protocol may have a compliant implementation for:
+
+ defimpl Size, for: BitString do
+ def size(binary), do: byte_size(binary)
+ end
+
+ defimpl Size, for: Map do
+ def size(map), do: map_size(map)
+ end
+
+ defimpl Size, for: Tuple do
+ def size(tuple), do: tuple_size(tuple)
+ end
+
+ Notice we didn't implement it for lists as we don't have the
+ `size` information on lists, rather its value needs to be
+ computed with `length`.
+
+ It is possible to implement protocols for all Elixir types:
+
+ * Structs (see below)
+ * `Tuple`
+ * `Atom`
+ * `List`
+ * `BitString`
+ * `Integer`
+ * `Float`
+ * `Function`
+ * `PID`
+ * `Map`
+ * `Port`
+ * `Reference`
+ * `Any` (see below)
+
+ ## Protocols and Structs
+
+ The real benefit of protocols comes when mixed with structs.
+ For instance, Elixir ships with many data types implemented as
+ structs, like `MapSet`. We can implement the `Size` protocol
+ for those types as well:
+
+ defimpl Size, for: MapSet do
+ def size(map_set), do: MapSet.size(map_set)
+ end
+
+ When implementing a protocol for a struct, the `:for` option can
+ be omitted if the `defimpl` call is inside the module that defines
+ the struct:
+
+ defmodule User do
+ defstruct [:email, :name]
+
+ defimpl Size do
+ # two fields
+ def size(%User{}), do: 2
+ end
+ end
+
+ If a protocol implementation is not found for a given type,
+ invoking the protocol will raise unless it is configured to
+ fall back to `Any`. Conveniences for building implementations
+ on top of existing ones are also available, look at `defstruct/1`
+ for more information about deriving
+ protocols.
+
+ ## Fallback to `Any`
+
+ In some cases, it may be convenient to provide a default
+ implementation for all types. This can be achieved by setting
+ the `@fallback_to_any` attribute to `true` in the protocol
+ definition:
+
+ defprotocol Size do
+ @fallback_to_any true
+ def size(data)
+ end
+
+ The `Size` protocol can now be implemented for `Any`:
+
+ defimpl Size, for: Any do
+ def size(_), do: 0
+ end
+
+ Although the implementation above is arguably not a reasonable
+ one. For example, it makes no sense to say a PID or an integer
+ have a size of `0`. That's one of the reasons why `@fallback_to_any`
+ is an opt-in behaviour. For the majority of protocols, raising
+ an error when a protocol is not implemented is the proper behaviour.
+
+ ## Multiple implementations
+
+ Protocols can also be implemented for multiple types at once:
+
+ defprotocol Reversible do
+ def reverse(term)
+ end
+
+ defimpl Reversible, for: [Map, List] do
+ def reverse(term), do: Enum.reverse(term)
+ end
+
+ Inside `defimpl/2`, you can use `@protocol` to access the protocol
+ being implemented and `@for` to access the module it is being
+ defined for.
+
+ ## Types
+
+ Defining a protocol automatically defines a type named `t`, which
+ can be used as follows:
+
+ @spec print_size(Size.t()) :: :ok
+ def print_size(data) do
+ result =
+ case Size.size(data) do
+ 0 -> "data has no items"
+ 1 -> "data has one item"
+ n -> "data has #{n} items"
+ end
+
+ IO.puts(result)
+ end
+
+ The `@spec` above expresses that all types allowed to implement the
+ given protocol are valid argument types for the given function.
+
+ ## Reflection
+
+ Any protocol module contains three extra functions:
+
+ * `__protocol__/1` - returns the protocol information. The function takes
+ one of the following atoms:
+
+ * `:consolidated?` - returns whether the protocol is consolidated
+
+ * `:functions` - returns keyword list of protocol functions and their arities
+
+ * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
+ implementing the protocol, otherwise `:not_consolidated`
+
+ * `:module` - the protocol module atom name
+
+ * `impl_for/1` - receives a structure and returns the module that
+ implements the protocol for the structure, `nil` otherwise
- Protocols do not allow functions to be defined directly, instead, the
- regular `Kernel.def/*` macros are replaced by this macro which
- defines the protocol functions with the appropriate callbacks.
+ * `impl_for!/1` - same as above but raises an error if an implementation is
+ not found
+
+ For example, for the `Enumerable` protocol we have:
+
+ iex> Enumerable.__protocol__(:functions)
+ [count: 1, member?: 2, reduce: 3, slice: 1]
+
+ iex> Enumerable.impl_for([])
+ Enumerable.List
+
+ iex> Enumerable.impl_for(42)
+ nil
+
+ ## Consolidation
+
+ In order to cope with code loading in development, protocols in
+ Elixir provide a slow implementation of protocol dispatching specific
+ to development.
+
+ In order to speed up dispatching in production environments, where
+ all implementations are known up-front, Elixir provides a feature
+ called protocol consolidation. Consolidation directly links protocols
+ to their implementations in a way that invoking a function from a
+ consolidated protocol is equivalent to invoking two remote functions.
+
+ Protocol consolidation is applied by default to all Mix projects during
+ compilation. This may be an issue during test. For instance, if you want
+ to implement a protocol during test, the implementation will have no
+ effect, as the protocol has already been consolidated. One possible
+ solution is to include compilation directories that are specific to your
+ test environment in your mix.exs:
+
+ def project do
+ ...
+ elixirc_paths: elixirc_paths(Mix.env())
+ ...
+ end
+
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ And then you can define the implementations specific to the test environment
+ inside `test/support/some_file.ex`.
+
+ Another approach is to disable protocol consolidation during tests in your
+ mix.exs:
+
+ def project do
+ ...
+ consolidate_protocols: Mix.env() != :test
+ ...
+ end
+
+ Although doing so is not recommended as it may affect your test suite
+ performance.
+
+ Finally note all protocols are compiled with `debug_info` set to `true`,
+ regardless of the option set by `elixirc` compiler. The debug info is
+ used for consolidation and it may be removed after consolidation.
"""
+
+ @doc false
defmacro def(signature)
defmacro def({_, _, args}) when args == [] or is_atom(args) do