Skip to content

Commit

Permalink
feat: Add 'sequence' and 'liftm' for maybe monads (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
moritzploss-k authored May 12, 2021
1 parent 12d7e7c commit d66ce8b
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 2 deletions.
30 changes: 28 additions & 2 deletions include/maybe.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%%_* Types ============================================================
-type functor(A) :: s2_functors:functor(A).
-type thunk(A) :: fun(() -> A).
-type functor(A) :: s2_functors:functor(A).
-type thunk(A) :: fun(() -> A).
-type collection(A) :: [A] | #{_ := A}.

-type ok(A) :: {ok, A}.
-type error(A) :: {error, A}.
Expand Down Expand Up @@ -91,6 +92,31 @@
-define(thunk(E0, E1, E2, E3, E4, E5, E6, E7, E8, E9),
fun() -> E0, E1, E2, E3, E4, E5, E6, E7, E8, E9 end).

-define(liftm(F, A1),
s2_maybe:liftm(F, [?thunk(A1)])).
-define(liftm(F, A1, A2),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2)])).
-define(liftm(F, A1, A2, A3),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3)])).
-define(liftm(F, A1, A2, A3, A4),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4)])).
-define(liftm(F, A1, A2, A3, A4, A5),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4),
?thunk(A5)])).
-define(liftm(F, A1, A2, A3, A4, A5, A6),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4),
?thunk(A5), ?thunk(A6)])).
-define(liftm(F, A1, A2, A3, A4, A5, A6, A7),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4),
?thunk(A5), ?thunk(A6), ?thunk(A7)])).
-define(liftm(F, A1, A2, A3, A4, A5, A6, A7, A8),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4),
?thunk(A5), ?thunk(A6), ?thunk(A7), ?thunk(A8)])).
-define(liftm(F, A1, A2, A3, A4, A5, A6, A7, A8, A9),
s2_maybe:liftm(F, [?thunk(A1), ?thunk(A2), ?thunk(A3), ?thunk(A4),
?thunk(A5), ?thunk(A6), ?thunk(A7), ?thunk(A8),
?thunk(A9)])).

%%%_* Guards ===========================================================
-define(is_thunk(X), is_function(X, 0)).

Expand Down
43 changes: 43 additions & 0 deletions src/s2_maybe.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
, fmap/2
, lift/1
, lift/2
, liftm/2
, map/2
, reduce/2
, reduce/3
, sequence/1
, to_bool/1
, unlift/1
, unlift/2
Expand Down Expand Up @@ -108,6 +110,20 @@ lift_unlift_test() ->
42 = unlift(fun(X) -> {ok, X} end, 42).
-endif.

-spec liftm(fun(), [maybe(_, B)] | [thunk(maybe(_, B))]) -> maybe(_, B).
%% @doc lift a function F into the Maybe monad.
liftm(F, Maybes) when is_list(Maybes) and is_function(F, length(Maybes)) ->
?fmap(fun(Args) -> apply(F, Args) end, sequence(Maybes)).

-ifdef(TEST).
liftm_test() ->
Add3 = fun(A, B, C) -> A + B + C end,
ValsOK = [{ok, 1}, {ok, 2}, {ok, 3}],
ValsError = [{ok, 1}, {error, reason}, {ok, 3}],
?assertEqual({ok, 6}, liftm(Add3, ValsOK)),
?assertEqual({error, reason}, liftm(Add3, ValsError)),
?assertEqual({ok, 6}, ?liftm(Add3, {ok, 1}, {ok, 2}, {ok, 3})).
-endif.

-spec map(fun(), [_]) -> maybe(_, _).
%%@doc map(F, Xs) is the result of mapping F over Xs inside the maybe
Expand Down Expand Up @@ -137,6 +153,33 @@ reduce_test() ->
{error, _} = reduce(fun(X, Y) -> X + Y end, [0, foo]).
-endif.

-spec sequence(collection(maybe(A, B)) | collection(thunk(maybe(A, B)))) -> maybe(collection(A), B).
%% @doc sequence(Maybes) evaluates each maybe in a collectiom of maybes and
%% collects the results inside the maybe monad. Supports lazy evaluation of
%% thunks.
sequence([F | Maybes]) when ?is_thunk(F) ->
sequence([F() | Maybes]);
sequence([{ok, Val} | Maybes]) ->
?fmap(fun(Vals) -> [Val | Vals] end, sequence(Maybes));
sequence([{error, Reason} | _Maybes]) ->
{error, Reason};
sequence([]) ->
?lift([]);
sequence(Map) when is_map(Map) ->
{Keys, Vals} = lists:unzip(maps:to_list(Map)),
?fmap(fun(Sequenced) -> maps:from_list(lists:zip(Keys, Sequenced)) end,
sequence(Vals)).

-ifdef(TEST).
sequence_test() ->
?assertEqual({ok, [1, 2, 3]}, sequence([{ok, 1}, {ok, 2}, {ok, 3}])),
?assertEqual({error, foo}, sequence([{ok, 1}, {error, foo}, {error, bar}])),
?assertEqual({ok, []}, sequence([])),
?assertEqual({error, foo}, sequence([?thunk({ok, 1}), ?thunk({error, foo})])),
?assertEqual({ok, #{a => 1, b => 2}}, sequence(#{a => {ok, 1}, b => {ok, 2}})),
?assertEqual({error, foo}, sequence(#{a => {ok, 1}, b => {error, foo}})),
?assertEqual({error, foo}, sequence(#{a => {ok, 1}, b => ?thunk({error, foo})})).
-endif.

-spec to_bool(maybe(_, _)) -> boolean().
%% @doc to_bool(X) is the boolean representation of the maybe-value X.
Expand Down

0 comments on commit d66ce8b

Please sign in to comment.