0

This program is crashing despite seeming to work. I don't understand why though. I'm trying to accomplish a deep merge and need conditional logic.

Given the following list:

    ManOne = #{ "Bob" => #{"Sagget" => #{}} }
    ManTwo = #{ "Bob" => #{"Daniels" => #{}} }

I'm trying to compare them as follows, this function returns true as expected:

check_if_same(M1, M2) -> 
   {ok, lists:sort( maps:keys(M1) ) == lists:sort( maps:keys(M2) )}.
merger(M1, M2) ->
   M1_Keys = maps:keys(M1),
   M2_Keys = maps:keys(M2),
   do_merge(M1, M2, M1_Keys).
do_merge(M1, M2, [Head|Tail]) ->
   Check = check_if_same(M1, M2),
   io:fwrite("Check is: ~p\n", [Check]),
   case Check of 
     {ok, true} -> 
       io:fwrite("true\n");
     {ok, false} ->
       io:fwrite("false\n")
   end,
   do_merge(M1, M2, Tail);
do_merge(M1, M2, []) -> 
   ok.
check_if_same(M1, M2) -> 
   {ok, lists:sort( maps:keys(M1) ) == lists:sort( maps:keys(M2) )}.

gives the following output:

Check is: {ok,true}
true
{"init terminating in do_boot",{{badmap,ok},[{maps,keys,[ok],[]},{helloworld,merger,2,[{file,"helloworld.erl"},{line,9}]},{init,start_em,1,[]},{init,do_boot,3,[]}]}}
init terminating in do_boot ()

Crash dump is being written to: erl_crash.dump...done
user3505901
  • 408
  • 1
  • 6
  • 19
  • Could you please either Accept my answer or Comment what I didn't properly explain (or what you didn't understand) so that I can edit my answer to be better in the future? Thanks :) – artman41 Jun 29 '20 at 17:14

2 Answers2

3

Let's go over your question first because there's a few misconceptions and/or corrections that we can make.

Your Description

This program is crashing despite seeming to work. I don't understand why though. I'm trying to accomplish a deep merge and need conditional logic.

Given the following list:

    ManOne = #{ "Bob" => #{"Sagget" => #{}} }
    ManTwo = #{ "Bob" => #{"Daniels" => #{}} }

Note that the above are NOT lists, they are maps, which function entirely differently.

A map is, for all intents and purposes, a lookup table until it contains ~31 key/value pairs.

At this point, it becomes a HashMap (this can be seen by viewing the elements as they become unordered after the map becomes a HashMap).

I'm trying to compare them as follows, this function returns true as expected:

check_if_same(M1, M2) -> 
   {ok, lists:sort( maps:keys(M1) ) == lists:sort( maps:keys(M2) )}.

This is an incorrect way to assert equality; in erlang, it is suggested to not use == to check equality.

Instead, =:= should be used.

The reason for this is due to the fact that == does NOT check the type of the elements it is comparing and only takes a fuzzy value - i.e 1 == 1.0 will return true but 1 =:= 1.0 will return false.

Personally, I would recommend instead using Erlang's Pattern-Matching to check your values.

This could be implemented using the following snippet:

-spec check_if_same(M1 :: map(), M2 :: map()) -> boolean().
check_if_same(M1, M2) ->
    SortedKeys1 = lists:sort(maps:keys(M1)),
    SortedKeys2 = lists:sort(maps:keys(M2)),
    %% We hide the implementation of the function in
    %%  a function with the same name suffixed with an
    %%  underscore. This allows us to have a public api
    %%  but keep the implementation internal which allows
    %%  the code to be a bit cleaner.
    check_if_same_(SortedKeys1, SortedKeys2). 

%% If they're both empty then we've gone through
%%  every key meaning that they must be identical
check_if_same_([], []) ->
    true;
%% If the current Key on both heads is the same
%%  then recurse and check the next and so on
check_if_same_([Key|Tail1], [Key|Tail2]) ->
    check_if_same_(Tail1, Tail2);
%% If we get anything else, e.g more keys in 
%%  one than the other or the keys don't match,
%%  then we'll fall in to this case. 
%% As we know anything that falls in to this 
%%  case doesn't match, we just return false
check_if_same_(Keys1, Keys2) when is_list(Keys1), is_list(Keys2) ->
    false.

Note that in the above snippet, I only ever returned true or false - my recommendation for cleaner code would be to keep to the following formats;

  • ok - This is typically for functions where you care about the effect and not the return
  • true | false - This is typically for comparison functions, i.e is_binary/1, is_function/1
  • {ok, Value} - This would typically be for any function where you care about the value returned
  • {error, Reason} - This would be used whenever you expect an error so that you can bubble the error back up the chain with an easy-to-match format

Your Code Snippet

merger(M1, M2) ->
    M1_Keys = maps:keys(M1),
    %% Note that you don't use the M2Keys here so you don't need to do the work to get them
    M2_Keys = maps:keys(M2),
    do_merge(M1, M2, M1_Keys).
do_merge(M1, M2, [Head|Tail]) ->
    Check = check_if_same(M1, M2),
    %% It's generally recommended to stick to io:format/2 rather than io:fwrite/2
    io:fwrite("Check is: ~p\n", [Check]), 
    case Check of 
        {ok, true} -> 
            io:fwrite("true\n");
        {ok, false} ->
            io:fwrite("false\n")
    end,
    do_merge(M1, M2, Tail);
do_merge(M1, M2, []) -> 
    ok.
check_if_same(M1, M2) -> 
    {ok, lists:sort( maps:keys(M1) ) == lists:sort( maps:keys(M2) )}.

Now, the above snippet (other than being a bit inefficient) is perfectly okay erlang and will work as expected

gives the following output:

Check is: {ok,true}
true
{"init terminating in do_boot",{{badmap,ok},[{maps,keys,[ok],[]},{helloworld,merger,2,[{file,"helloworld.erl"},{line,9}]},{init,start_em,1,[]},{init,do_boot,3,[]}]}}
init terminating in do_boot ()

Crash dump is being written to: erl_crash.dump...done

This crash dump is where the real issue is;

Check is: {ok,true}
true

From this we can tell that we

  • Hit the io:fwrite/2 (io:fwrite("Check is: ~p\n", [Check]))
  • Entered the {ok, true} path in the case (io:fwrite("true\n"))

The next line is where we see the actual problem, let's break it down:

  • "init terminating in do_boot" - We failed when starting up, this might be when running an escript or starting an app

Now let's break down that tuple:

{
    {badmap,ok}, %% The function we called expected a map and we passed in 'ok'
    [
        {maps,keys,[ok],[]}, %% We called maps:keys/1 with 'ok' as an arg
        {helloworld,merger,2,[{file,"helloworld.erl"},{line,9}]}, %% This was called at helloworld:merger/2 (helloworld.erl:9)
        {init,start_em,1,[]},{init,do_boot,3,[]} %% We failed on start up
    ]
}

What we can take away from this is that you're calling merger in your code with an invalid value of ok on line 9 of helloworld.erl

artman41
  • 402
  • 2
  • 5
  • 15
-1

There is some missing information. Although this code looks like a first draft or step, it works as expected. I tested it in the shell and got this:

-module (merger).

-compile(export_all).

merger(M1, M2) ->
   M1_Keys = maps:keys(M1),
   M2_Keys = maps:keys(M2),
   do_merge(M1, M2, M1_Keys).
do_merge(M1, M2, [Head|Tail]) ->
   Check = check_if_same(M1, M2),
   io:fwrite("Check is: ~p\n", [Check]),
   case Check of 
     {ok, true} -> 
       io:fwrite("true\n");
     {ok, false} ->
       io:fwrite("false\n")
   end,
   do_merge(M1, M2, Tail);
do_merge(M1, M2, []) -> 
   ok.
check_if_same(M1, M2) -> 
   {ok, lists:sort( maps:keys(M1) ) == lists:sort( maps:keys(M2) )}.

test() ->
 merger(#{ "Bob" => #{"Sagget" => #{}} },#{ "Bob" => #{"Daniels" => #{}} }).

which gives:

8> c(merger).
merger.erl:3: Warning: export_all flag enabled - all functions will be exported
merger.erl:7: Warning: variable 'M2_Keys' is unused
merger.erl:9: Warning: variable 'Head' is unused
merger.erl:19: Warning: variable 'M1' is unused
merger.erl:19: Warning: variable 'M2' is unused
{ok,merger}
9> merger:test().
Check is: {ok,true}
true
ok
10>

Maybe you could also tell us what is the expected result of merging ManOne and ManTwo

Pascal
  • 13,977
  • 2
  • 24
  • 32