Current section
Files
Jump to
Current section
Files
src/function.erl
%%% @doc
%%% Function utilities.
%%%
%%% This module provides utilities for working with functions, including
%%% function introspection, composition, currying, and other functional
%%% programming patterns.
%%%
%%% Examples:
%%% ```
%%% AddTwo = function:curry(fun erlang:'+'/2, [2]),
%%% 5 = AddTwo(3),
%%%
%%% AddThenDouble = function:compose([fun(X) -> X * 2 end, fun(X) -> X + 1 end]),
%%% 8 = AddThenDouble(3). % (3 + 1) * 2 = 8
%%% '''
%%% @end
-module('function').
%% API
-export([exported/3, info/1, info/2]).
-export([identity/1, constant/1]).
-export([apply/2, apply/3, apply/4]).
-export([curry/2, partial/2]).
-export([compose/1, pipe/2]).
-export([negate/1, complement/1]).
-export([memoize/1, throttle/2]).
%% Types
-type fun_info() :: arity | env | index | name | module | new_index |
new_uniq | pid | reductions | type | uniq.
-export_type([fun_info/0]).
%%% @doc
%%% Checks if a function is exported by a module.
%%%
%%% Returns true if the function is exported, false otherwise.
%%% @end
-spec exported(module(), atom(), arity()) -> boolean().
exported(Module, Function, Arity) ->
erlang:function_exported(Module, Function, Arity).
%%% @doc
%%% Returns information about the given function.
%%%
%%% Returns a list of {Key, Value} tuples with function information.
%%% @end
-spec info(fun()) -> [{fun_info(), term()}].
info(Fun) when is_function(Fun) ->
erlang:fun_info(Fun).
%%% @doc
%%% Returns specific information about the given function.
%%%
%%% Returns the value associated with the given key, or undefined if not found.
%%% @end
-spec info(fun(), fun_info()) -> term() | undefined.
info(Fun, Item) when is_function(Fun) ->
try
case erlang:fun_info(Fun, Item) of
{Item, Value} -> Value
end
catch
error:badarg -> undefined
end.
%%% @doc
%%% The identity function - returns its argument unchanged.
%%%
%%% Useful as a default function or in functional composition.
%%% @end
-spec identity(term()) -> term().
identity(X) -> X.
%%% @doc
%%% Returns a function that always returns the given constant value.
%%%
%%% Useful for creating constant functions in functional patterns.
%%% @end
-spec constant(term()) -> fun(() -> term()).
constant(Value) ->
fun() -> Value end.
%%% @doc
%%% Applies a function to a list of arguments.
%%%
%%% This is equivalent to erlang:apply/2 but with a more convenient name.
%%% @end
-spec apply(fun(), [term()]) -> term().
apply(Fun, Args) when is_function(Fun), is_list(Args) ->
erlang:apply(Fun, Args).
%%% @doc
%%% Applies a function from a module to a list of arguments.
%%%
%%% This is equivalent to erlang:apply/3 but with a more convenient name.
%%% @end
-spec apply(module(), atom(), [term()]) -> term().
apply(Module, Function, Args) ->
erlang:apply(Module, Function, Args).
%%% @doc
%%% Applies a function to up to 4 arguments directly.
%%%
%%% More efficient than building an argument list for simple cases.
%%% @end
-spec apply(fun(), term(), term(), term()) -> term().
apply(Fun, Arg1, Arg2, Arg3) when is_function(Fun, 3) ->
Fun(Arg1, Arg2, Arg3);
apply(Fun, Arg1, Arg2, Arg3) when is_function(Fun, 4) ->
Fun(Arg1, Arg2, Arg3, undefined).
%%% @doc
%%% Curries a function by partially applying some arguments.
%%%
%%% Returns a new function that expects the remaining arguments.
%%% @end
-spec curry(fun(), [term()]) -> fun().
curry(Fun, Args) when is_function(Fun), is_list(Args) ->
{arity, Arity} = erlang:fun_info(Fun, arity),
NumApplied = length(Args),
case NumApplied of
N when N >= Arity ->
% All arguments provided, apply immediately
erlang:apply(Fun, Args);
_ ->
% Create a new function expecting remaining arguments
RemainingArity = Arity - NumApplied,
create_curried_fun(Fun, Args, RemainingArity)
end.
%%% @doc
%%% Partially applies a function with the given arguments.
%%%
%%% Similar to curry/2 but always returns a function regardless of arity.
%%% @end
-spec partial(fun(), [term()]) -> fun().
partial(Fun, Args) when is_function(Fun), is_list(Args) ->
fun(RestArgs) ->
AllArgs = Args ++ RestArgs,
erlang:apply(Fun, AllArgs)
end.
%%% @doc
%%% Composes a list of functions into a single function.
%%%
%%% Functions are applied right-to-left (mathematical composition).
%%% The last function in the list is applied first.
%%% @end
-spec compose([fun()]) -> fun().
compose([]) ->
fun identity/1;
compose([Fun]) ->
Fun;
compose([Fun | Rest]) ->
RestComposed = compose(Rest),
fun(X) -> Fun(RestComposed(X)) end.
%%% @doc
%%% Pipes a value through a list of functions.
%%%
%%% Functions are applied left-to-right (pipeline style).
%%% @end
-spec pipe(term(), [fun()]) -> term().
pipe(Value, []) ->
Value;
pipe(Value, [Fun | Rest]) ->
pipe(Fun(Value), Rest).
%%% @doc
%%% Returns a function that negates the result of the given function.
%%%
%%% The given function should return a boolean value.
%%% @end
-spec negate(fun()) -> fun().
negate(Fun) when is_function(Fun) ->
{arity, Arity} = erlang:fun_info(Fun, arity),
create_negated_fun(Fun, Arity).
%%% @doc
%%% Returns a function that returns the boolean complement of the given function.
%%%
%%% Alias for negate/1 for better readability in some contexts.
%%% @end
-spec complement(fun()) -> fun().
complement(Fun) ->
negate(Fun).
%%% @doc
%%% Creates a memoized version of the given function.
%%%
%%% The memoized function caches results based on input arguments.
%%% Uses the process dictionary for caching - not suitable for long-lived processes.
%%% @end
-spec memoize(fun()) -> fun().
memoize(Fun) when is_function(Fun) ->
CacheKey = make_ref(),
{arity, Arity} = erlang:fun_info(Fun, arity),
create_memoized_fun(Fun, CacheKey, Arity).
%%% @doc
%%% Creates a throttled version of the given function.
%%%
%%% The throttled function can only be called once per interval (in milliseconds).
%%% @end
-spec throttle(fun(), non_neg_integer()) -> fun().
throttle(Fun, IntervalMs) when is_function(Fun), is_integer(IntervalMs) ->
ThrottleKey = make_ref(),
{arity, Arity} = erlang:fun_info(Fun, arity),
create_throttled_fun(Fun, ThrottleKey, IntervalMs, Arity).
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%% @private
create_curried_fun(Fun, Args, 1) ->
fun(Arg) ->
AllArgs = Args ++ [Arg],
erlang:apply(Fun, AllArgs)
end;
create_curried_fun(Fun, Args, 2) ->
fun(Arg1, Arg2) ->
AllArgs = Args ++ [Arg1, Arg2],
erlang:apply(Fun, AllArgs)
end;
create_curried_fun(Fun, Args, 3) ->
fun(Arg1, Arg2, Arg3) ->
AllArgs = Args ++ [Arg1, Arg2, Arg3],
erlang:apply(Fun, AllArgs)
end;
create_curried_fun(Fun, Args, _Arity) ->
% For higher arities, use a variadic approach
fun(RestArgs) when is_list(RestArgs) ->
AllArgs = Args ++ RestArgs,
erlang:apply(Fun, AllArgs)
end.
%% @private
create_negated_fun(Fun, 0) ->
fun() -> not Fun() end;
create_negated_fun(Fun, 1) ->
fun(Arg) -> not Fun(Arg) end;
create_negated_fun(Fun, 2) ->
fun(Arg1, Arg2) -> not Fun(Arg1, Arg2) end;
create_negated_fun(Fun, 3) ->
fun(Arg1, Arg2, Arg3) -> not Fun(Arg1, Arg2, Arg3) end;
create_negated_fun(Fun, _Arity) ->
fun(Args) when is_list(Args) ->
not erlang:apply(Fun, Args)
end.
%% @private
create_memoized_fun(Fun, CacheKey, 0) ->
fun() ->
case get({memoize, CacheKey, []}) of
undefined ->
Result = Fun(),
put({memoize, CacheKey, []}, Result),
Result;
CachedResult ->
CachedResult
end
end;
create_memoized_fun(Fun, CacheKey, 1) ->
fun(Arg) ->
Key = {memoize, CacheKey, [Arg]},
case get(Key) of
undefined ->
Result = Fun(Arg),
put(Key, Result),
Result;
CachedResult ->
CachedResult
end
end;
create_memoized_fun(Fun, CacheKey, 2) ->
fun(Arg1, Arg2) ->
Key = {memoize, CacheKey, [Arg1, Arg2]},
case get(Key) of
undefined ->
Result = Fun(Arg1, Arg2),
put(Key, Result),
Result;
CachedResult ->
CachedResult
end
end;
create_memoized_fun(Fun, CacheKey, _Arity) ->
fun(Args) when is_list(Args) ->
Key = {memoize, CacheKey, Args},
case get(Key) of
undefined ->
Result = erlang:apply(Fun, Args),
put(Key, Result),
Result;
CachedResult ->
CachedResult
end
end.
%% @private
create_throttled_fun(Fun, ThrottleKey, IntervalMs, 0) ->
fun() ->
Now = erlang:system_time(millisecond),
LastCallKey = {throttle, ThrottleKey, last_call},
case get(LastCallKey) of
undefined ->
put(LastCallKey, Now),
Fun();
LastCall when Now - LastCall >= IntervalMs ->
put(LastCallKey, Now),
Fun();
_LastCall ->
% Too soon, return cached result or error
case get({throttle, ThrottleKey, last_result}) of
undefined -> error(throttled);
LastResult -> LastResult
end
end
end;
create_throttled_fun(Fun, ThrottleKey, IntervalMs, 1) ->
fun(Arg) ->
Now = erlang:system_time(millisecond),
LastCallKey = {throttle, ThrottleKey, last_call},
case get(LastCallKey) of
undefined ->
put(LastCallKey, Now),
Result = Fun(Arg),
put({throttle, ThrottleKey, last_result}, Result),
Result;
LastCall when Now - LastCall >= IntervalMs ->
put(LastCallKey, Now),
Result = Fun(Arg),
put({throttle, ThrottleKey, last_result}, Result),
Result;
_LastCall ->
% Too soon, return cached result or error
case get({throttle, ThrottleKey, last_result}) of
undefined -> error(throttled);
LastResult -> LastResult
end
end
end;
create_throttled_fun(Fun, ThrottleKey, IntervalMs, _Arity) ->
fun(Args) when is_list(Args) ->
Now = erlang:system_time(millisecond),
LastCallKey = {throttle, ThrottleKey, last_call},
case get(LastCallKey) of
undefined ->
put(LastCallKey, Now),
Result = erlang:apply(Fun, Args),
put({throttle, ThrottleKey, last_result}, Result),
Result;
LastCall when Now - LastCall >= IntervalMs ->
put(LastCallKey, Now),
Result = erlang:apply(Fun, Args),
put({throttle, ThrottleKey, last_result}, Result),
Result;
_LastCall ->
% Too soon, return cached result or error
case get({throttle, ThrottleKey, last_result}) of
undefined -> error(throttled);
LastResult -> LastResult
end
end
end.