%% ============================================================================= %% resulto.erl - %% Copyright (c) 2016-2024 Leapsight. All rights reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% ============================================================================= %% ----------------------------------------------------------------------------- %% @doc Result represents the result of something that may succeed or not. %% `{ok, any()}` means it was successful, `{error, any()}` means it was not. %% %% Borrowed from the elegant [homonimous Gleam module] %% (https://hexdocs.pm/gleam_stdlib/gleam/result.html). %% @end %% ----------------------------------------------------------------------------- -module(resulto). -type t() :: ok | ok() | error(). -type ok() :: ok(any()). -type error() :: error(any()). -type ok(T) :: {ok, T}. -type error(T) :: {error, T}. -export(['try'/2]). -export([all/1]). -export([error/1]). -export([flatten/1]). -export([is_error/1]). -export([is_ok/1]). -export([lazy_or/2]). -export([lazy_unwrap/2]). -export([map/2]). -export([map_error/2]). -export([ok/1]). -export([or_else/2]). -export([partition/1]). -export([raise_or/1]). -export([raise_or_unwrap/1]). -export([replace/2]). -export([replace_error/2]). -export([result/1]). -export([then/2]). -export([then_both/3]). -export([then_recover/2]). -export([try_both/3]). -export([try_recover/2]). -export([undefined_error/1]). -export([unwrap/2]). -export([unwrap_both/1]). -export([unwrap_error/2]). -export([values/1]). -compile({no_auto_import, [error/1]}). %% ============================================================================= %% API %% ============================================================================= %% ----------------------------------------------------------------------------- %% @doc %% @end %% ----------------------------------------------------------------------------- -spec ok(Value :: any()) -> ok(). ok(Value) -> {ok, Value}. %% ----------------------------------------------------------------------------- %% @doc %% @end %% ----------------------------------------------------------------------------- -spec error(Error :: any()) -> error(). error(Error) -> {error, Error}. %% ----------------------------------------------------------------------------- %% @doc %% @end %% ----------------------------------------------------------------------------- -spec result(Term :: any()) -> t(). result(ok) -> ok; result({ok, _} = T) -> T; result({error, _} = T) -> T; result(Term) -> ok(Term). %% ----------------------------------------------------------------------------- %% @doc Combines a list of results into a single result. %% If all elements in the list are `ok` then returns an `ok` holding the list of %% values. If any element is `errore then returns the first `error`. %% @end %% ----------------------------------------------------------------------------- -spec all([t()]) -> t(). all(Results) when is_list(Results) -> try Values = lists:foldl( fun ({ok, Value}, Acc) -> [Value | Acc]; ({error, _} = Result, _) -> throw(Result) end, [], Results ), {ok, lists:reverse(Values)} catch throw:{error, _} = Result -> Result end. %% ----------------------------------------------------------------------------- %% @doc Merges a nested result into a single layer. %% @end %% ----------------------------------------------------------------------------- -spec flatten(t()) -> t(). flatten({ok, {ok, _} = Result}) -> flatten(Result); flatten({ok, {error, _} = Result}) -> flatten(Result); flatten({error, {error, _} = Result}) -> flatten(Result); flatten({ok, _} = Result) -> Result; flatten({error, _} = Result) -> Result. %% ----------------------------------------------------------------------------- %% @doc Checks whether the result is an `error` value. %% @end %% ----------------------------------------------------------------------------- -spec is_error(t()) -> boolean(). is_error(ok) -> false; is_error({ok, _}) -> false; is_error({error, _}) -> true. %% ----------------------------------------------------------------------------- %% @doc Checks whether the result is an `ok` value. %% @end %% ----------------------------------------------------------------------------- -spec is_ok(t()) -> boolean(). is_ok(ok) -> true; is_ok({ok, _}) -> true; is_ok({error, _}) -> false. %% ----------------------------------------------------------------------------- %% @doc Returns the first value if it is `ok`, otherwise evaluates the given %% function for a fallback value. %% @end %% ----------------------------------------------------------------------------- -spec lazy_or(Result :: t(), Fun :: fun(() -> t())) -> t(). lazy_or(ok = Result, Fun) when is_function(Fun, 0) -> Result; lazy_or({ok, _} = Result, Fun) when is_function(Fun, 0) -> Result; lazy_or({error, _}, Fun) when is_function(Fun, 0) -> Fun(). %% ----------------------------------------------------------------------------- %% @doc Extracts the `ok` value from a result, evaluating the default function %% if the result is an `error`. %% @end %% ----------------------------------------------------------------------------- -spec lazy_unwrap(Result :: t(), Fun :: fun(() -> t())) -> any(). lazy_unwrap(ok, Fun) when is_function(Fun, 0) -> undefined; lazy_unwrap({ok, Value}, Fun) when is_function(Fun, 0) -> Value; lazy_unwrap({error, _}, Fun) when is_function(Fun, 0) -> Fun(). %% ----------------------------------------------------------------------------- %% @doc Returns the result if it is `ok`, otherwise raises an exception with the %% error value. %% @end %% ----------------------------------------------------------------------------- -spec raise_or(t()) -> ok() | no_return(). raise_or(ok = Result) -> Result; raise_or({ok, _} = Result) -> Result; raise_or({error, Reason}) -> erlang:error(Reason). %% ----------------------------------------------------------------------------- %% @doc Returns the result value if it is `ok`, otherwise raises an exception %% with the error value. %% @end %% ----------------------------------------------------------------------------- -spec raise_or_unwrap(t()) -> any() | no_return(). raise_or_unwrap(ok) -> undefined; raise_or_unwrap({ok, Value}) -> Value; raise_or_unwrap({error, Reason}) -> erlang:error(Reason). %% ----------------------------------------------------------------------------- %% @doc Updates a value held within the `ok` of a result by calling a given %% function on it. %% If the result is an `error` rather than `ok` the function is not called and %% the result stays the same. %% @end %% ----------------------------------------------------------------------------- -spec map(Result :: t(), fun((any()) -> any())) -> error() | any(). map(ok, Fun) when is_function(Fun, 1) -> {ok, Fun(undefined)}; map({ok, Value}, Fun) when is_function(Fun, 1) -> {ok, Fun(Value)}; map({error, _} = Result, Fun) when is_function(Fun, 1) -> Result. %% ----------------------------------------------------------------------------- %% @doc Updates a value held within the `error` of a result by calling a given %% function on it. %% If the result is `ok`rather than `error` the function is not called and the %% result stays the same. %% @end %% ----------------------------------------------------------------------------- -spec map_error(Result :: t(), fun((any()) -> any())) -> error() | any(). map_error(ok = Result, Fun) when is_function(Fun, 1) -> Result; map_error({ok, _} = Result, Fun) when is_function(Fun, 1) -> Result; map_error({error, Error}, Fun) when is_function(Fun, 1) -> {error, Fun(Error)}. %% ----------------------------------------------------------------------------- %% @doc Transforms any error into `error(undefined)`. %% @end %% ----------------------------------------------------------------------------- -spec undefined_error(Result :: t()) -> ok() | error(undefined). undefined_error(ok = Result) -> Result; undefined_error({ok, _} = Result) -> Result; undefined_error({error, _}) -> error(undefined). %% ----------------------------------------------------------------------------- %% @doc Returns the first value if it is `ok`, otherwise returns the second %% value. %% @end %% ----------------------------------------------------------------------------- -spec or_else(First :: t(), Second :: t()) -> t(). or_else(ok = Result, _) -> Result; or_else(_, ok = Result) -> Result; or_else({ok, _} = Result, _) -> Result; or_else(_, {ok, _} = Result) -> Result; or_else(_, {error, _} = Result) -> Result. %% ----------------------------------------------------------------------------- %% @doc Given a list of results, returns a pair where the first element is a %% list of all the values inside `ok` and the second element is a list with all %% the values inside `error`. %% The values in both lists appear in reverse order with respect to their %% position in the original list of results. %% @end %% ----------------------------------------------------------------------------- -spec partition(Results :: [t()]) -> {[any()], [any()]} | no_return(). partition(Results) -> lists:foldl( fun (ok, Acc) -> Acc; ({ok, Value}, {Values, Errors}) -> {[Value | Values], Errors}; ({error, Error}, {Values, Errors}) -> {Values, [Error | Errors]}; (_, _) -> error(badarg) end, {[], []}, Results ). %% ----------------------------------------------------------------------------- %% @doc Replace the value within a result. %% @end %% ----------------------------------------------------------------------------- -spec replace(Result :: t(), Value :: any()) -> t(). replace(ok, Value) -> {ok, Value}; replace({ok, _}, Value) -> {ok, Value}; replace({error, _} = Result, _) -> Result. %% ----------------------------------------------------------------------------- %% @doc Replace the error within a result %% @end %% ----------------------------------------------------------------------------- -spec replace_error(Result :: t(), Error :: any()) -> t(). replace_error(ok = Result, _) -> Result; replace_error({ok, _} = Result, _) -> Result; replace_error({error, _}, Error) -> {error, Error}. %% ----------------------------------------------------------------------------- %% @doc An alias for `try/2`. %% @end %% ----------------------------------------------------------------------------- -spec then(Result :: t(), Fun :: fun((any()) -> t())) -> t() | no_return(). then(Result, Fun) -> 'try'(Result, Fun). %% ----------------------------------------------------------------------------- %% @doc An alias for `try_recover/2`. %% @end %% ----------------------------------------------------------------------------- -spec then_recover(Result :: t(), Fun :: fun((any()) -> t())) -> t() | no_return(). then_recover(Result, Fun) -> try_recover(Result, Fun). %% ----------------------------------------------------------------------------- %% @doc An alias for `try_both/3`. %% @end %% ----------------------------------------------------------------------------- -spec then_both( Result :: t(), Fun :: fun((any()) -> t()), Fun :: fun((any()) -> t())) -> t() | no_return(). then_both(Result, Fun, RecoverFun) -> try_both(Result, Fun, RecoverFun). %% ----------------------------------------------------------------------------- %% @doc “Updates” an `ok` result by passing its value to a function that yields %% \a result, and returning the yielded result. (This may “replace” the `ok` %% with an `error`). %% %% If the input is an `error` rather than an `ok`, the function is not called %% and the original `error` is returned. %% %% This function is the equivalent of calling `map/2` followed by `flatten/1`, %% and it is useful for chaining together multiple functions that may fail. %% @end %% ----------------------------------------------------------------------------- -spec 'try'(Result :: t(), Fun :: fun((any()) -> t())) -> t() | no_return(). 'try'(ok = Result, Fun) when is_function(Fun, 1) -> eval_result(Result, Fun); 'try'({ok, _} = Result, Fun) when is_function(Fun, 1) -> eval_result(Result, Fun); 'try'({error, _} = Result, Fun) when is_function(Fun, 1) -> Result. %% ----------------------------------------------------------------------------- %% @doc Updates a value held within the Error of a result by calling a given %% function on it, where the given function also returns a result. The two %% results are then merged together into one result. %% %% If the result is an Ok rather than Error the function is not called and the %% result stays the same. %% %% This function is useful for chaining together computations that may fail and %% trying to recover from possible errors. %% @end %% ----------------------------------------------------------------------------- -spec try_recover(Result :: t(), Fun :: fun((any()) -> t())) -> t() | no_return(). try_recover(ok = Result, Fun) when is_function(Fun, 1) -> Result; try_recover({ok, _} = Result, Fun) when is_function(Fun, 1) -> Result; try_recover({error, _} = Result, Fun) when is_function(Fun, 1) -> eval_result(Result, Fun). %% ----------------------------------------------------------------------------- %% @doc Updates an `ok' result by passing its value to a `fun' that yields %% a result, and returning the yielded result. If the input is an `error' rather %% than an `ok', updates a value held within the `error` of a result by calling %% `recover_fun' on it, where the given function also returns a result. %% @end %% ----------------------------------------------------------------------------- -spec try_both( Result :: t(), Fun :: fun((any()) -> t()), Fun :: fun((any()) -> t())) -> t() | no_return(). try_both(ok = Result, Fun, _) -> 'try'(Result, Fun); try_both({ok, _} = Result, Fun, _) -> 'try'(Result, Fun); try_both({error, _} = Result, _, RecoverFun) -> try_recover(Result, RecoverFun). %% ----------------------------------------------------------------------------- %% @doc Extracts the Ok value from a result, returning a default value if the %% result is an Error. %% @end %% ----------------------------------------------------------------------------- -spec unwrap(Result :: t(), Default :: any()) -> any(). unwrap(ok, _) -> undefined; unwrap({ok, Value}, _) -> Value; unwrap({error, _}, Default) -> Default. %% ----------------------------------------------------------------------------- %% @doc Extracts the inner value from a result. %% @end %% ----------------------------------------------------------------------------- -spec unwrap_both(Result :: t()) -> any(). unwrap_both(ok) -> undefined; unwrap_both({ok, Value}) -> Value; unwrap_both({error, Error}) -> Error. %% ----------------------------------------------------------------------------- %% @doc Extracts the Error value from a result, returning a default value if the %% result is an Ok. %% @end %% ----------------------------------------------------------------------------- -spec unwrap_error(Result :: t(), Default :: any()) -> any(). unwrap_error(ok, Default) -> Default; unwrap_error({ok, _}, Default) -> Default; unwrap_error({error, Error}, _) -> Error. %% ----------------------------------------------------------------------------- %% @doc Given a list of results, returns only the values inside Ok. %% @end %% ----------------------------------------------------------------------------- -spec values(Results :: [t()]) -> [any()]. values(Results) -> lists:filtermap( fun ({ok, Value}) -> {true, Value}; (_) -> false end, Results ). %% ============================================================================= %% PRIVATE %% ============================================================================= eval_result(Result0, Fun) when is_function(Fun, 0) -> case Fun() of ok = Result -> Result; {ok, _} = Result -> Result; {error, _} = Result -> Result; _ -> not_a_result([Result0, Fun], 2) end; eval_result(ok, Fun) when is_function(Fun, 1) -> case Fun(undefined) of ok = Result -> Result; {ok, _} = Result -> Result; {error, _} = Result -> Result; _ -> not_a_result([ok, Fun], 2) end; eval_result(Result0, Fun) when is_function(Fun, 1) -> case Fun(element(2, Result0)) of ok = Result -> Result; {ok, _} = Result -> Result; {error, _} = Result -> Result; _ -> not_a_result([Result0, Fun], 2) end. not_a_result(Args, Pos) -> erlang:error( badarg, Args, [ {error_info, #{ module => ?MODULE, cause => #{ Pos => "Function should return a 'bondy_result:t()'." }, meta => #{} }} ] ). %% ============================================================================= %% EUNIT %% ============================================================================= -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). all_test_() -> [ ?_assertEqual(ok([1, 2]), all([ok(1), ok(2)])), ?_assertEqual(error(foo), all([ok(1), ok(2), error(foo)])), ?_assertEqual(error(foo), all([ok(1), ok(2), error(foo), error(bar)])), ?_assertEqual(error(foo), all([error(foo), error(bar)])), ?_assertError(function_clause, all([foo])) ]. flatten_test_() -> [ ?_assertEqual(ok(foo), flatten(ok(ok(foo)))), ?_assertEqual(ok(foo), flatten(ok(ok(ok(ok(foo)))))), ?_assertEqual(error(foo), flatten(error(error(foo)))), ?_assertEqual(error(foo), flatten(error(error(error(error(foo)))))), ?_assertEqual(error(foo), flatten(ok(error(foo)))), ?_assertError(function_clause, flatten(foo)) ]. typecheck_test_() -> [ ?_assert(is_ok(ok(foo))), ?_assertEqual(false, is_ok(error(foo))), ?_assert(is_error(error(foo))), ?_assertEqual(false, is_error(ok(foo))), ?_assertError(function_clause, is_ok(foo)), ?_assertError(function_clause, is_error(foo)) ]. lazy_or_test_() -> [ ?_assertEqual(ok(1), lazy_or(ok(1), fun() -> ok(2) end)), ?_assertEqual(ok(1), lazy_or(ok(1), fun() -> error(foo) end)), ?_assertEqual(ok(1), lazy_or(error(foo), fun() -> ok(1) end)), ?_assertEqual(error(bar), lazy_or(error(foo), fun() -> error(bar) end)), ?_assertEqual(ok(1), lazy_or(ok(1), fun() -> not_a_result end)), ?_assertEqual(not_a_result, lazy_or(error(foo), fun() -> not_a_result end)), ?_assertError(function_clause, lazy_or(ok(1), not_a_fun)), ?_assertError(function_clause, lazy_or(error(foo), not_a_fun)) ]. lazy_unwrap_test_() -> [ ?_assertEqual(1, lazy_unwrap(ok(1), fun() -> 2 end)), ?_assertEqual(2, lazy_unwrap(error(foo), fun() -> 2 end)), ?_assertError(function_clause, lazy_unwrap(ok(1), not_a_fun)), ?_assertError(function_clause, lazy_unwrap(error(foo), not_a_fun)) ]. map_test_() -> [ ?_assertEqual(ok(2), map(ok(1), fun(X) -> X + 1 end)), ?_assertEqual(error(foo), map(error(foo), fun(X) -> X + 1 end)), ?_assertError(function_clause, map(ok(1), not_a_fun)), ?_assertError(function_clause, map(error(foo), not_a_fun)) ]. undefined_error_test_() -> [ ?_assertEqual(ok(1), undefined_error(ok(1))), ?_assertEqual(error(undefined), undefined_error(error(foo))) ]. or_test_() -> [ ?_assertEqual(ok(1), or_else(ok(1), ok(2))), ?_assertEqual(ok(1), or_else(ok(1), error(foo))), ?_assertEqual(ok(2), or_else(error(foo), ok(2))), ?_assertEqual(error(bar), or_else(error(foo), error(bar))) ]. partition_test_() -> [ ?_assertEqual( {[2, 1], [bar, foo]}, partition([ok(1), error(foo), ok(2), error(bar)]) ) ]. replace_test_() -> [ ?_assertEqual(ok(2), replace(ok(1), 2)), ?_assertEqual(error(foo), replace(error(foo), 2)) ]. replace_error_test_() -> [ ?_assertEqual(ok(1), replace_error(ok(1), bar)), ?_assertEqual(error(bar), replace_error(error(foo), bar)) ]. then_try_test_() -> [ ?_assertEqual(ok(2), then(ok(1), fun(X) -> ok(X + 1) end)), ?_assertEqual(ok(2), 'try'(ok(1), fun(X) -> ok(X + 1) end)), ?_assertEqual(error("Oh no"), then(ok(1), fun(_) -> error("Oh no") end)), ?_assertEqual(error("Oh no"), 'try'(ok(1), fun(_) -> error("Oh no") end)) ]. try_recover_test_() -> [ ?_assertEqual(ok(1), try_recover(ok(1), fun(_) -> error("Oh no") end)), ?_assertEqual(ok(2), try_recover(error(1), fun(X) -> ok(X + 1) end)), ?_assertEqual( error("Oh no"), try_recover(error(1), fun(_) -> error("Oh no") end) ) ]. unwrap_test_() -> [ ?_assertEqual(1, unwrap(ok(1), 2)), ?_assertEqual(2, unwrap(error(1), 2)) ]. unwrap_both_test_() -> [ ?_assertEqual(1, unwrap_both(ok(1))), ?_assertEqual(1, unwrap_both(error(1))) ]. unwrap_error_test_() -> [ ?_assertEqual(2, unwrap_error(ok(1), 2)), ?_assertEqual(1, unwrap_error(error(1), 2)) ]. values_test_() -> [ ?_assertEqual([1, 2], values([ok(1), ok(2), error(foo)])), ?_assertEqual([], values([error(foo)])) ]. -endif.