From 07c2b75b3184aa6c64284fde5d52f450c53927cd Mon Sep 17 00:00:00 2001 From: Alex Bubnov Date: Fri, 12 Jan 2018 21:06:39 +0300 Subject: [PATCH] Maps: implement clojure-style *_in accessors. When key is missing nad no default provided or non-map value fetched somewhere on path these functions will behave like ones from maps module - throw. --- src/genlib_map.erl | 71 +++++++++++++++++++++++++++++++++++++-- test/genlib_map_tests.erl | 50 +++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/genlib_map.erl b/src/genlib_map.erl index 3bcff37..37744f8 100644 --- a/src/genlib_map.erl +++ b/src/genlib_map.erl @@ -18,6 +18,9 @@ -export([binarize/2]). -export([diff/2]). +-export([get_in/2, get_in/3, mget_in/2]). +-export([put_in/3]). +-export([update_in/3, update_in/4]). %% -spec get(any(), map()) -> undefined | term(). @@ -57,8 +60,9 @@ deepput([Key], Value, Map) -> deepput([Key | Rest], Value, Map) -> maps:put(Key, deepput(Rest, Value, get(Key, Map, #{})), Map). --spec truemap(Function, map()) -> ok when - Function :: fun((Key :: any(), Value :: any()) -> {Key :: any(), Value :: any()}). +-spec truemap(Function, map()) -> #{any() => any()} + when + Function :: fun((Key :: any(), Value :: any()) -> {Key :: any(), Value :: any()}). truemap(F, Map = #{}) -> maps:fold(fun (K, V, M) -> {Kn, Vn} = F(K, V), maps:put(Kn, Vn, M) end, #{}, Map). @@ -111,3 +115,66 @@ decrement(N) -> N - 1. diff(Map, Since) -> maps:fold(fun (K, V, M) -> case get(K, M, make_ref()) of V -> maps:remove(K, M); _ -> M end end, Map, Since). + + +%% clojure-style accessors + +-spec get_in(KeyPath::[term()], Map::map()) -> term(). + +get_in(Path, Map) when is_list(Path), is_map(Map) -> + lists:foldl(fun maps:get/2, Map, Path). + + +-spec get_in(KeyPath::[term()], Map::map(), Default::term()) -> term(). + +get_in(Path, Map, Default) when is_list(Path), is_map(Map) -> + get_in(make_ref(), Path, Map, Default). + +get_in(Ref, _, Ref, Default) -> Default; +get_in(_, [], Val, _) -> Val; +get_in(_, _, NotMap, Default) when not is_map(NotMap) -> Default; +get_in(Ref, [H | T], M, D) -> get_in(Ref, T, maps:get(H, M, Ref), D). + + +-type path() :: [any()] | {[any()], Default::any()}. +-spec mget_in(Paths, map()) -> [any()] when + Paths :: [path()] | #{any() => path()}. + +mget_in(Paths, Map) when is_list(Paths), is_map(Map) -> + lists:map(fun + ({K, D}) -> get_in(K, Map, D); + (K) -> get_in(K, Map) + end, Paths); + +mget_in(Paths, Map) when is_map(Paths), is_map(Map) -> + maps:map(fun + (_, {Path, D}) -> get_in(Path, Map, D); + (_, Path) -> get_in(Path, Map) + end, Paths). + + +-spec put_in(KeyPath::[term()], Value::term(), map()) -> map(). + +put_in(Path, Value, Map) -> + deepput(Path, Value, Map). + + +-spec update_in(Key::[any()], Fun :: fun((any()) -> any()), map()) -> map(). + +update_in([Key], Fun, Map) -> + maps:put(Key, Fun(maps:get(Key, Map)), Map); + +update_in([Key | Rest], Fun, Map) -> + maps:put(Key, update_in(Rest, Fun, get(Key, Map, #{})), Map). + + +-spec update_in(Key::[any()], Fun::fun((any()) -> any()), Init::term(), map()) -> map(). + +update_in([Key], Fun, Init, Map) -> + maps:put(Key, case maps:find(Key, Map) of + {ok, Value} -> Fun(Value); + _ -> Init + end, Map); + +update_in([Key | Rest], Fun, Init, Map) -> + maps:put(Key, update_in(Rest, Fun, Init, get(Key, Map, #{})), Map). diff --git a/test/genlib_map_tests.erl b/test/genlib_map_tests.erl index 21fc3c8..ee379ae 100644 --- a/test/genlib_map_tests.erl +++ b/test/genlib_map_tests.erl @@ -40,3 +40,53 @@ diff_test_() -> ?_assertEqual(#{this_is => 'undefined'}, genlib_map:diff(#{this_is => 'undefined'}, #{this_is => 'not'})), ?_assertEqual(#{this_is => 'not' }, genlib_map:diff(#{this_is => 'not'}, #{this_is => 'undefined'})) ]. + +get_in_test_() -> + M = #{a => 1, b => 2, c => #{d => 3, e => 4}, g => false}, + R = make_ref(), + [ + ?_assertEqual(M, genlib_map:get_in([], M)), + ?_assertEqual(1, genlib_map:get_in([a], M)), + ?_assertEqual(false, genlib_map:get_in([g], M)), + ?_assertEqual(4, genlib_map:get_in([c, e], M)), + + ?_assertError({badkey, e}, genlib_map:get_in([e], M)), + ?_assertError({badkey, x}, genlib_map:get_in([c, x], M)), + ?_assertError(function_clause, genlib_map:get_in(notlist, M)), + ?_assertError(function_clause, genlib_map:get_in(notlist, M, R)), + + ?_assertEqual(M, genlib_map:get_in([], M, 0)), + ?_assertEqual(R, genlib_map:get_in([e], M, R)), + ?_assertEqual(R, genlib_map:get_in([c, x], M, R)) + ]. + +mget_in_test_() -> + M = #{a => 1, b => 2, c => #{d => 3, e => 4}, g => false}, + R = make_ref(), + [ + ?_assertEqual([1, 4], genlib_map:mget_in([ [a], [c, e]], M)), + ?_assertEqual([1, R], genlib_map:mget_in([ [a], {[c, x], R}], M)), + + ?_assertEqual( + #{k1 => 1, k2 => R}, + genlib_map:mget_in(#{k1 => [a], k2 =>{[c, x], R}}, M)) + ]. + +update_in_test_() -> + [ + ?_assertEqual( + #{a => #{b => #{c => 42}}}, + genlib_map:update_in([a, b, c], fun(C) -> C+1 end, #{a => #{b => #{c => 41}}})), + + ?_assertError( + {badkey, c}, + genlib_map:update_in([a, b, c], fun(C) -> C+1 end, #{})), + ?_assertError( + {badmap, 44}, + genlib_map:update_in([a, b, c], fun(C) -> C+1 end, #{a => #{b => 44}})), + + + ?_assertEqual( + #{a => #{b => #{c => 42}}}, + genlib_map:update_in([a, b, c], fun(C) -> C+1 end, 42, #{})) + ].