Packages

A lightweight Erlang library for working with result types, implementing result monads for better error handling and chaining operations. It is based on elegant Gleam module 'result'.

Current section

Files

Jump to
resulto src resulto.erl
Raw

src/resulto.erl

%% =============================================================================
%% resulto.erl -
%% Copyright (c) 2024-2025 Leapsight. All rights reserved.
%% Copyright 2018, Louis Pilfold <louis@lpil.uk>.
%%
%% 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.
%% =============================================================================
-module(resulto).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-moduledoc #{format => "text/markdown"}.
?MODULEDOC("""
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 Gleam's [result]
(https://hexdocs.pm/gleam_stdlib/gleam/result.html) module.
""").
?DOC("""
Represents a value that is either a success (`ok`, `{ok, Value}`) or a
failure (`{error, Reason}`).
""").
-type t() :: ok | ok() | error().
?DOC("A successful result with its associated value").
-type ok() :: ok(any()).
?DOC("A failure result with its associated reason").
-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([throw_or/1]).
-export([throw_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/1]).
-export([unwrap/2]).
-export([unwrap_both/1]).
-export([unwrap_error/2]).
-export([values/1]).
-compile({no_auto_import, [error/1]}).
%% =============================================================================
%% API
%% =============================================================================
?DOC("Creates a successful result.").
-spec ok(Value :: any()) -> ok().
ok(Value) ->
{ok, Value}.
?DOC("Creates a failed result.").
-spec error(Error :: any()) -> error().
error(Error) ->
{error, Error}.
?DOC("""
Wraps a term in a result.
If the term is already a result, it is returned as-is. If the term is `ok`,
it returns `ok`; otherwise it wraps the term in `{ok, Term}`.
""").
-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 an error then returns the first `error`.
""").
-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("""
Flattens nested results (e.g., `{ok, {ok, Value}}`) into a single-layer result.
""").
-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("Returns `true` if the result is an error.").
-spec is_error(t()) -> boolean().
is_error(ok) -> false;
is_error({ok, _}) -> false;
is_error({error, _}) -> true.
?DOC("Returns `true` if the result is an successful.").
-spec is_ok(t()) -> boolean().
is_ok(ok) -> true;
is_ok({ok, _}) -> true;
is_ok({error, _}) -> false.
?DOC("""
Returns the original result if it's `ok`, otherwise calls the provided function
to return an alternative result.
""").
-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("""
Returns the value of the result if it's `ok`, otherwise calls the fallback
function.
""").
-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 successful, otherwise raises the error.
""").
-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("Extracts the `ok` value or raises the error if not present.").
-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("""
Returns the result if successful, otherwise throws the error.
""").
-spec throw_or(t()) -> ok() | no_return().
throw_or(ok = Result) ->
Result;
throw_or({ok, _} = Result) ->
Result;
throw_or({error, Reason}) ->
throw(Reason).
?DOC("Extracts the `ok` value or raises the error if not present.").
-spec throw_or_unwrap(t()) -> any() | no_return().
throw_or_unwrap(ok) ->
undefined;
throw_or_unwrap({ok, Value}) ->
Value;
throw_or_unwrap({error, Reason}) ->
throw(Reason).
?DOC("""
Applies a function to the value inside `ok`. Errors are returned unchanged.
""").
-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("""
Applies a function to the value inside `error`. Successful results are
returned unchanged.
""").
-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("Converts any error to `{error, undefined}`.").
-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 result if successful, otherwise the second.").
-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("""
Separates a list of results into a tuple with all successes and all errors.
The lists are in reverse order of their appearance.
""").
-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("Replaces the value inside a successful result.").
-spec replace(Result :: t(), Value :: any()) -> t().
replace(ok, Value) -> {ok, Value};
replace({ok, _}, Value) -> {ok, Value};
replace({error, _} = Result, _) -> Result.
?DOC("Replaces the value inside a failed result.").
-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`.").
-spec then(Result :: t(), Fun :: fun((any()) -> t())) -> t() | no_return().
then(Result, Fun) -> 'try'(Result, Fun).
?DOC("An alias for `try_recover/2`.").
-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/2`.").
-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("""
Applies a function to the value inside a successful result, returning its result.
Errors are returned unchanged.
""").
-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("""
Applies a function to the value inside an error, returning its result.
Success values are returned unchanged.
""").
-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("""
Applies a function to the value inside a successful result.
If it's an error, applis a recovery function instead.
""").
-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("""
Returns the `ok` value or `undefined` if the result is an error.
""").
-spec unwrap(Result :: t()) -> any().
unwrap(Result) ->
unwrap(Result, undefined).
?DOC("""
Returns the `ok` value or the given default value if it's an error.
""").
-spec unwrap(Result :: t(), Default :: any()) -> any().
unwrap(ok, _) -> undefined;
unwrap({ok, Value}, _) -> Value;
unwrap({error, _}, Default) -> Default.
?DOC("""
Returns either the `ok` or `error` value, whichever is present.
""").
-spec unwrap_both(Result :: t()) -> any().
unwrap_both(ok) -> undefined;
unwrap_both({ok, Value}) -> Value;
unwrap_both({error, Error}) -> Error.
?DOC("""
Returns the `error` value or the default if it's an `ok`.
""").
-spec unwrap_error(Result :: t(), Default :: any()) -> any().
unwrap_error(ok, Default) -> Default;
unwrap_error({ok, _}, Default) -> Default;
unwrap_error({error, Error}, _) -> Error.
?DOC("""
Returns all the values inside `ok` results from a list of results.
""").
-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.