Current section
Files
Jump to
Current section
Files
src/optic_generic.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% A set of generic optics that can be applied to multiple container
%%% types.
%%%
%%% Intended both as a convenience and to support optic creation from
%%% parsed paths in optic_path.
%%%
%%% Because of the ambiguous types they support, these optics do not
%%% support the standard optic options. Instead, they always skip
%%% unexpected types and never create missing values.
%%% @end
%%%-------------------------------------------------------------------
-module(optic_generic).
%% API
-export([key/1,
index/1]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc
%% Focus on the value of many different key/value like mappings.
%% Understands how to focus on maps, property lists, dicts, orddicts
%% and gb_trees. Does not support the usual optic options.
%%
%% Example:
%%
%% ```
%% > optic:get([optic_generic:key(first)], #{first => 1}).
%% {ok,[1]}
%% '''
%% @end
%% @param Key The key to focus on.
%% @returns An opaque optic record.
-spec key(term()) -> optic:optic().
key(Key) ->
Fold =
fun (Fun, Acc, Map) when is_map(Map) ->
case Map of
#{Key:=Value} ->
{ok, Fun(Value, Acc)};
_ ->
{ok, Acc}
end;
(Fun, Acc, List) when is_list(List) ->
% This might be a proplist or an orddict.
Values = proplists:get_all_values(Key, List),
{ok, lists:foldl(Fun, Acc, Values)};
(Fun, Acc, Unknown) ->
case {is_dict(Unknown), is_gb_tree(Unknown)} of
{true, _} ->
% It's bad if is_dict & is_gb_tree ever both return
% true, but I can't justify throwing an error when
% this is already doing such fuzzy guessing of types.
Dict = Unknown,
case dict:find(Key, Dict) of
{ok, Value} ->
{ok, Fun(Value, Acc)};
error ->
{ok, Acc}
end;
{false, true} ->
Tree = Unknown,
case gb_trees:lookup(Key, Tree) of
{value, Value} ->
{ok, Fun(Value, Acc)};
none ->
{ok, Acc}
end;
{false, false} ->
{ok, Acc}
end
end,
MapFold =
fun (Fun, Acc, Map) when is_map(Map) ->
case Map of
#{Key:=Value} ->
{NewValue, NewAcc} = Fun(Value, Acc),
{ok, {Map#{Key:=NewValue}, NewAcc}};
_ ->
{ok, {Map, Acc}}
end;
(Fun, Acc, List) when is_list(List) ->
% This might be a proplist or an orddict.
{ok, lists:mapfoldl(fun (Elem, InnerAcc) ->
case proplists:is_defined(Key, [Elem]) of
true ->
Value = proplists:get_value(Key, [Elem]),
{NewValue, NewAcc} = Fun(Value, InnerAcc),
{{Key, NewValue}, NewAcc};
false ->
{Elem, InnerAcc}
end
end,
Acc,
List)};
(Fun, Acc, Unknown) ->
case {is_dict(Unknown), is_gb_tree(Unknown)} of
{true, _} ->
% It's bad if is_dict & is_gb_tree ever both return
% true, but I can't justify throwing an error when
% this is already doing such fuzzy guessing of types.
Dict = Unknown,
case dict:find(Key, Dict) of
{ok, Value} ->
{NewValue, NewAcc} = Fun(Value, Acc),
{ok, {dict:store(Key, NewValue, Dict), NewAcc}};
error ->
{ok, {Dict, Acc}}
end;
{false, true} ->
Tree = Unknown,
case gb_trees:lookup(Key, Tree) of
{value, Value} ->
{NewValue, NewAcc} = Fun(Value, Acc),
{ok, {gb_trees:update(Key, NewValue, Tree), NewAcc}};
none ->
{ok, {Tree, Acc}}
end;
{false, false} ->
{ok, {Unknown, Acc}}
end
end,
optic:new(MapFold, Fold).
%% @doc
%% Focus on an element of a list like container. Indexing begins at 1.
%% Understands how to focus on lists and tuples. Does not support the
%% usual optic options.
%%
%% Example:
%%
%% ```
%% > optic:get([optic_generic:index(3)], [1, 2, 3]).
%% {ok,[3]}
%% '''
%% @end
%% @param Index The one based index of the element to focus on.
%% @returns An opaque optic record.
-spec index(non_neg_integer()) -> optic:optic().
index(Index) ->
Fold =
fun (Fun, Acc, List) when Index =< length(List) ->
Elem = lists:nth(Index, List),
{ok, Fun(Elem, Acc)};
(Fun, Acc, Tuple) when Index =< tuple_size(Tuple) ->
Elem = erlang:element(Index, Tuple),
{ok, Fun(Elem, Acc)};
(_Fun, Acc, _Data) ->
{ok, Acc}
end,
MapFold =
fun (Fun, Acc, List) when Index =< length(List) ->
{Before, [Head | Tail]} = lists:split(Index - 1, List),
{NewHead, NewAcc} = Fun(Head, Acc),
{ok, {Before ++ [NewHead] ++ Tail, NewAcc}};
(Fun, Acc, Tuple) when Index =< tuple_size(Tuple) ->
Elem = erlang:element(Index, Tuple),
{NewElem, NewAcc} = Fun(Elem, Acc),
{ok, {erlang:setelement(Index, Tuple, NewElem), NewAcc}};
(_Fun, Acc, Unknown) ->
{ok, {Unknown, Acc}}
end,
optic:new(MapFold, Fold).
%%%===================================================================
%%% Internal Functions
%%%===================================================================
is_dict(Unknown) ->
try dict:size(Unknown) of
_ ->
true
catch
error:function_clause ->
false
end.
is_gb_tree(Unknown) ->
try gb_trees:size(Unknown) of
_ ->
true
catch
error:function_clause ->
false
end.