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

function_definition()
function_definition() :: %{name: :atom, kind: kind, args: [Macro.t], guards: [Macro.t], body: Macro.t, file: binary, line: integer, ast: Macro.t}

The function definition is derived from @on_definition arguments

include_opt()
include_opt ::
  {:include, names} |
  {:filter, maybe_quoted_filter_fun} |
  {:mapper, maybe_quoted_mapper_fun} |
  {:module, maybe_quoted_module}

The opts for the include action

include_opts()
include_opts() :: [include_opt]
kind()
kind() :: :def | :defp | :defmacro | :defmacrop
maybe_quoted_filter_fun()

Maybe quoted filter fun

Maybe quoted mapper fun

maybe_quoted_module()
maybe_quoted_module() :: module | unquotable

Maybe quoted module

name()
name() :: atom

The names of the functions to register or include.

names()
names() :: name | [name]
register_opt()
register_opt ::
  {:register, names} |
  {:module, maybe_quoted_module}

The opts for the register action

register_opts()
register_opts() :: [register_opt]
unquotable()
unquotable() :: Macro.t

Something that can be unquoted

use_option()
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.

use_options()
use_options() :: [use_option]

Functions

reconstruct_function(definition)
reconstruct_function(function_definition) :: function_definition

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

__using__(opts \\ [])
__using__(term, use_options) :: [Macro.t]

Registering or Including Function Definitions

Options

  • :includethe 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.