siariwyd v0.2.0 Siariwyd
A module for sharing and reusing callback functions.
The most obvious use case is the callback functions of
GenServer
i.e. handle_code/3
, handle_cast/2
and
handle_info/2
.
Callback function sources have to be compiled together in the callback module to enable multiple implementations of the same callback to be found by pattern matching.
Although targetting GenServer
callbacks, Siariwyd
can be used to
share any function’s implementations (callback or otherwise) that
must be compiled together.
Siariwyd
enables one or more implementations of a function to be
register-ed in one or more donor modules and selectively
include-d into a recipient (e.g. callback) module at
compilation time.
Example
Below are two example GenServer
donor modules, one (DonorA)
with one handle_call/3
and one handle_cast/2
, and the other
(DonorB) with one handle_call/3
implementations:
defmodule DonorA do
use GenServer
Use Siariwyd, register: [:handle_call, :handle_cast, :handle_info]
def handle_call({:donor_a_call1, value}, _fromref, state) do
{:reply, value, state}
end
def handle_cast({:donor_a_cast3, value}, state) do
{:noreply, state}
end
end
defmodule DonorB do
use GenServer
Use Siariwyd, register: [:handle_call, :handle_cast, :handle_info]
def handle_call({:donor_b_call1, value}, _fromref, state) do
{:reply, value, state}
end
end
The use call:
use Siariwyd, register: [:handle_call, :handle_cast, :handle_info]
causes all of the handle_call/3
, handle_cast/2
and handle_info/2
definitions found in the DonorA and DonorB modules to be saved in
each module’s compiled (BEAM) file.
Siariwyd does not insist on finding implementations for
register-ed functions - there are e.g. no handle_info/1
functions in either of the donor modules.
The register-ed implementations can be include-d into a recipient module. Again, if no implementations are found in the donor module(s), the callback is ignored.
defmodule Recipient1 do
use GenServer
# <handle calls start here>
use Siariwyd, module: DonorA, include: :handle_call
use Siariwyd, module: DonorB, include: :handle_call
def handle_call({:recipient_call1, value}, _fromref, state) do
{:reply, value, state}
end
# <handle calls finish here>
# <handle casts start here>
use Siariwyd, module: DonorA, include: :handle_cast
def handle_call({:recipient_cast3, value}, _fromref, state) do
{:reply, state}
end
# <handle casts finish here>
# <handle_infos start here>
# no definitions will be found and these uses ignored
use Siariwyd, module: DonorA, include: :handle_info
use Siariwyd, module: DonorB, include: :handle_info
# <handle_infos finish here>
end
Implementations are include-d first in the order of the uses in the recipient module and then the order in which they appear in the donor module(s).
A Function’s Implementation Definition
A function’s implementation definition is a map holding
most of the arguments passed to Module
compiler callback
@on_definition, together with only the file
and line
fields from the
env
argument.
It also has a key :ast
holding the full implementation of the
function; the value of the :ast
key is included when no mapper
(see later) is given.
The complete list of keys are:
:name
the name of the function (e.g. handle_call):kind
:def, :defp, :defmacro, or :defmacrop:args
the list of quoted arguments:guards
list of quoted guards:body
the quoted function body:file
the source file of the function:line
the source file line number of the function:ast
the complete ast of the function
Default is to Include all Implementations for all Registered Functions
If neither :include
nor :register
options are given, the default is
to include all implementations for all registered functions in
the donor module. e.g.
use Siariwyd, module: DonorA
Filtering the Included Implementations
A :filter
function (filter/1
) can be given to refine the
selection of the wanted implementations.
The :filter
function is used with Enum.filter/2
and is passed a
function implementation definition.
For example this function would select only handle_call/3
definitions:
use Siariwyd, module: DonorX,
filter: fn
%{name: :handle_call} -> true
_ -> false
end
A filter/1
function is applied after the implementations to
include have been selected (i.e. the :include
option functions or all
implementations for all registered functions).
Transforming (mapping) the Included Definitions
A :mapper
function (mapper/1
) can be given to transform the
wanted implementations.
The mapper/1
is passed a function’s implementation definition
and must return a definition, normally with the :ast
for the
complete implementation updated.
Siariwyd provides a convenience function
Siariwyd.reconstruct_function/1
to rebuild the :ast
from the
(updated) definition.
The example below shows how a handle_info/2
responding to
:process_message could be changed to respond to :analyse_message
use Siariwyd, module: DonorY,
mapper: fn
# select handle_info definition
%{name: :handle_info, args: args} = implementation_definition ->
# args is the list of quoted arguments to the implementation
# e.g. handle_info(:process_message, state)
[arg | rest_args] = args
case arg do
:process_message ->
# change the args to respond to :analyse_message
implementation_definition
|> Map.put(:args, [:analyse_message | rest_args])
# reconstruct the complete implementation using the convenience function
|> Siariwyd.reconstruct_function
# nothing to do
_ -> implementation_definition
end
# passthru
implementation_definition -> implementation_definition
end
A mapper/1
is applied after any filter/1
.
Summary
Types
The function definition is derived from @on_definition arguments
The opts for the include action
Maybe quoted filter fun
Maybe quoted mapper fun
Maybe quoted module
The names of the functions to register or include
The opts for the register action
Something that can be unquoted
The options passed to the use call
Functions
This convenenience function completely rebuilds the function’s implementation (an ast)
Macros
Registering or Including Function Definitions
Types
include_opt :: {:include, names} | {:filter, maybe_quoted_filter_fun} | {:mapper, maybe_quoted_mapper_fun} | {:module, maybe_quoted_module}
The opts for the include action
maybe_quoted_filter_fun :: unquotable | ({name, function_definition} -> as_boolean(term))
Maybe quoted filter fun
maybe_quoted_mapper_fun :: unquotable | ({name, function_definition} -> unquotable)
Maybe quoted mapper fun
Maybe quoted module
The opts for the register action
Something that can be unquoted
use_option :: {:include, names} | {:register, names} | {:filter, maybe_quoted_filter_fun} | {:mapper, maybe_quoted_mapper_fun} | {:module, module}
The options passed to the use call.
Functions
This convenenience function completely rebuilds the function’s implementation (an ast).
It is intended for use in mapper/1
functions.
Its takes the function’s definition as argument, reconstructs
the implementation’s ast, saves the ast under the definition’s :ast
key, and returns the updated definition.
Macros
Registering or Including Function Definitions
Options
:include
the names of the functions to include from the module.:register
the names of the function to save in the module.:module
the name of the module to include the registered functions from.:filter
a function to filter the:include
function definitions.:mapper
a function to transform the:include
function definitions.