%%% @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.