diff options
author | José Valim <jose.valim@plataformatec.com.br> | 2019-02-13 10:52:47 +0100 |
---|---|---|
committer | José Valim <jose.valim@plataformatec.com.br> | 2019-02-13 10:52:47 +0100 |
commit | bf3f4b5d6d023e843beb9c73ed894a06a5ebed3d (patch) | |
tree | 702ecbce51a058f90079edb8f2a2e9db872bf7a7 | |
parent | 6e7e9a994a967698d1645eb01e3635728347da94 (diff) | |
download | elixir-bf3f4b5d6d023e843beb9c73ed894a06a5ebed3d.tar.gz |
Consolidate Protocols information in the protocol module
-rw-r--r-- | lib/elixir/lib/kernel.ex | 232 | ||||
-rw-r--r-- | lib/elixir/lib/protocol.ex | 240 |
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 |