Exaop v0.1.0 Exaop View Source

A minimal library for aspect-oriented programming.

Unlike common AOP patterns, Exaop does not introduce any additional behavior to existing functions, as it may bring complexity and make the control flow obscured. Elixir developers prefer explicit over implicit, thus invoking the cross-cutting behavior by simply calling the plain old function generated by pointcut definitions is better than using some magic like module attributes and macros to decorate and weave a function.

Examples

Here's a simple example of using Exaop:

defmodule Foo do
  use Exaop

  check :validity
  set :compute

  @impl true
  def check_validity(%{b: b} = _params, _args, _acc) do
    if b == 0 do
      {:error, :divide_by_zero}
    else
      :ok
    end
  end

  @impl true
  def set_compute(%{a: a, b: b} = _params, _args, acc) do
    Map.put(acc, :result, a / b)
  end
end

A function __inject__/2 is generated in the above module Foo. When it is called, the callbacks are triggered in the order defined by your pointcut definitions.

Throughout the execution of the pointcut callbacks, an accumulator is passed and updated after running each callback. The execution process may be halted by a return value of a callback.

If the execution is not halted by any callback, the final accumulator value is returned by the __inject__/2 function. Otherwise, the return value of the callback that terminates the entire execution process is returned.

In the above example, the value of the accumulator is returned if the check_validity is passed:

iex> params = %{a: 1, b: 2}
iex> initial_acc = %{}
iex> Foo.__inject__(params, initial_acc)
%{result: 0.5}

The halted error is returned if the execution is aborted:

iex> params = %{a: 1, b: 0}
iex> initial_acc = %{}
iex> Foo.__inject__(params, initial_acc)
{:error, :divide_by_zero}

Pointcut definitions

check :validity
set :compute

We've already seen the pointcut definitions in the example before. check_validity/3 and set_compute/3 are the pointcut callback functions required by these definitions.

Additional arguments can be set:

check :validity, some_option: true
set :compute, {:a, :b}

Pointcut callbacks

Naming and arguments

All types of callbacks have the same function signature. Each callback function following the naming convention in the example, using an underscore to connect the pointcut type and the following atom as the callback function name.

Each callback has three arguments and each argument can be of any Elixir term.

The first argument of the callback function is passed from the first argument of the caller __inject__/2. The argument remains unchanged in each callback during the execution process.

The second argument of the callback function is passed from its pointcut definition, for example, set :compute, :my_arg passes :my_arg as the second argument of its callback function set_compute/3.

The third argument is the accumulator. It is initialized as the second argument of the caller __inject__/2. The value of accumulator is updated or remains the same after each callback execution, depending on the types and the return values of the callback functions.

Types and behaviours

Each kind of pointcut has different impacts on the execution process and the accumulator.

Exaop ships with three pointcut macros:

View documentation of these macros for details.

Link to this section Summary

Functions

Allows to decide whether to terminate the entire execution process of the generated __inject__/2 function. It does not change the value of the accumulator.

Allows to update the value of the accumulator or halt the execution process.

Allows to update the value of the accumulator, setting the accumulator to the return value of its callback. It does not halt the execution process.

Link to this section Types

Specs

acc() :: map() | struct()

Specs

args() :: term()

Specs

params() :: term()

Link to this section Functions

Link to this macro

check(target, args \\ nil, opts \\ [])

View Source (macro)

Allows to decide whether to terminate the entire execution process of the generated __inject__/2 function. It does not change the value of the accumulator.

The execution of the generated function is halted if its callback return value matches the pattern {:error, _}. The execution continues if its callback returns :ok.

Link to this macro

preprocess(target, args \\ nil, opts \\ [])

View Source (macro)

Allows to update the value of the accumulator or halt the execution process.

The execution of the generated function is halted if its callback return value matches the pattern {:error, _}. The accumulator is updated to the wrapped acc if its callback return value matches the pattern {:ok, acc}.

Link to this macro

set(target, args \\ nil, opts \\ [])

View Source (macro)

Allows to update the value of the accumulator, setting the accumulator to the return value of its callback. It does not halt the execution process.