Packages

A library to support a number of constructs for forming function callbacks

Current section

Files

Jump to
callback lib callback.ex
Raw

lib/callback.ex

defmodule Callback do
@typedoc """
A module-function-arity tuple with an explicit arity.
"""
@type mfa(arity) :: { module, atom, arity }
@typedoc """
A module-function-parameter tuple.
This either contains a list of parameters that will be passed to the function,
and any input parameters will be added following those parameters. Or an input
index or list of indexes will be provided (in which case those inputs will be
inserted into the parameter list at those positions). Or `nil` inputs, if no
inputs should be included, this however is only applicable to calls where the
input requirements `:optional`.
{ Foo, :bar, [:a, :b] }
\# If the callback is not passing any inputs then this function will
\# be called as:
Foo.bar(:a, :b)
\# If the callback is passing in 1 input ("hello"), then this function
\# will be called as:
Foo.bar(:a, :b, "hello")
\# If the callback is passing in 2 inputs ("hello", "world"), then this
\# function will be called as:
Foo.bar(:a, :b, "hello", "world")
{ Foo, :bar, [:a, :b], 0 }
\# If the callback is not passing any inputs then this function will
\# be called as:
Foo.bar(:a, :b)
\# If the callback is passing in 1 input ("hello"), then this function
\# will be called as:
Foo.bar("hello", :a, :b)
\# If the callback is passing in 2 inputs ("hello", "world"), then this
\# function will be called as:
Foo.bar("hello", "world", :a, :b)
{ Foo, :bar, [:a, :b], [0, 2] }
\# If the callback is not passing any inputs then this function will
\# be called as:
Foo.bar(:a, :b)
\# If the callback is passing in 1 input ("hello"), then this function
\# will be called as:
Foo.bar("hello", :a, :b)
\# If the callback is passing in 2 inputs ("hello", "world"), then this
\# function will be called as:
Foo.bar("hello", :a, "world", :b)
{ Foo, :bar, [:a, :b], nil }
\# If the callback is not passing any inputs then this function will
\# be called as:
Foo.bar(:a, :b)
\# If the callback is passing in 1 input ("hello"), then this function
\# will be called as:
Foo.bar(:a, :b)
\# If the callback is passing in 2 inputs ("hello", "world"), then this
\# function will be called as:
Foo.bar(:a, :b)
"""
@type mfp :: { module, atom, list } | { module, atom, list, nil | non_neg_integer | [non_neg_integer] }
@typedoc """
A generic callback form with any amount of arguments.
"""
@type callback :: fun | mfa | mfp
@typedoc """
An explicit callback form expecting 1 argument of the provided type, and
returning a result of the provided type.
"""
@type callback(arg1, result) :: (arg1 -> result) | mfa(1) | mfp
@typedoc """
An explicit callback form expecting 2 arguments of the provided type, and
returning a result of the provided type.
"""
@type callback(arg1, arg2, result) :: (arg1, arg2 -> result) | mfa(2) | mfp
@doc """
Returns `true` if `term` is a `callback`; otherwise returns `false`.
Allowed in guard tests.
"""
defguard is_callback(callback)
when is_tuple(callback)
and ((tuple_size(callback) == 3 and is_atom(elem(callback, 0)) and is_atom(elem(callback, 1)) and (is_integer(elem(callback, 2)) or is_list(elem(callback, 2))))
or (tuple_size(callback) == 4 and is_atom(elem(callback, 0)) and is_atom(elem(callback, 1)) and is_list(elem(callback, 2)) and (is_integer(elem(callback, 3)) or is_list(elem(callback, 3)) or is_nil(elem(callback, 3)))))
or is_function(callback)
@doc """
Call the provided callback with the given inputs.
The inputs to the callback are required by default but can otherwise be
made optional by passing `:optional` as the `input_requirement`.
"""
@spec call(callback, list, :required | :optional) :: any
def call(fun, inputs \\ [], input_requirement \\ :required)
def call({ module, fun, args, nil }, _, :optional) do
apply(module, fun, args)
end
def call({ module, fun, args, index }, inputs, _) when is_integer(index) do
{ left, right } = Enum.split(args, index)
apply(module, fun, left ++ inputs ++ right)
end
def call({ module, fun, args, indexes }, inputs, _) do
args = Enum.zip(indexes, inputs) |> Enum.sort |> insert_args(args)
apply(module, fun, args)
end
def call({ module, fun, arity }, inputs, _) when is_integer(arity) and length(inputs) == arity do
apply(module, fun, inputs)
end
def call({ module, fun, args }, inputs, _) when is_list(args) do
apply(module, fun, args ++ inputs)
end
def call(fun, inputs, _) when is_function(fun, length(inputs)) do
apply(fun, inputs)
end
defp insert_args(indexed, list, n \\ 0, result \\ [])
defp insert_args([], [], _, args), do: Enum.reverse(args)
defp insert_args([{ _, e }|indexed], [], n, args), do: insert_args(indexed, [], n + 1, [e|args])
defp insert_args([{ n, e }|indexed], list, n, args), do: insert_args(indexed, list, n + 1, [e|args])
defp insert_args(indexed, [e|list], n, args), do: insert_args(indexed, list, n + 1, [e|args])
end