MapDiff: A small library that computes the difference between two maps

Hello everyone,

I wrote a small library today called MapDiff.
It returns a map listing the (smallest amount of) changes to get from map_a to map_b:

iex> foo = %{a: 1, b: 2, c: %{d: 3, e: 4, f: 5}}
iex> bar = %{a: 1, b: 42, c: %{d: %{something_else: "entirely"}, f: 10}}
iex> MapDiff.diff(foo, bar)
%{changed: :map_change,
  value: %{a: %{changed: :equal, value: 1},
    b: %{added: 42, changed: :primitive_change, removed: 2},
    c: %{changed: :map_change,
      value: %{d: %{added: %{something_else: "entirely"},
      changed: :primitive_change, removed: 3},
    e: %{changed: :removed, value: 4},
    f: %{added: 10, changed: :primitive_change, removed: 5}}}}}

This is mainly useful if you have two maps describing the state of something, where
replacing all of the old state and then filling in the new is too slow, which means that only
replacing the things that actually are changed is better.

It is similar to what Elm does to represent its output to the HTML Domain Object Model. The reason I wrote MapDiff is that I am planning on writing a library that wraps Erlang’s wxWidgets bindings in a similar way that Elm wraps the HTML DOM, which would allow people to write GUIs in a Functional style (wxWidgets itself is Object-Oriented). Does this sound like a huge undertaking? Yes! Let’s see how far I can get with it :grin:


Anyhow, MapDiff is a very small and basic library, but it might be useful for some other people as well, so I thought I’d publish it on Hex.pm :slight_smile: . Feedback is of course greatly appreciated.

13 Likes

That seems like a useful simple library! Good job @Qqwy!

1 Like

That would be awesome and is entirely possible! I’d thought about it myself but wrapping to a QT layer instead. ^.^

This library will be useful to me though, I already have my more specialized one currently. :slight_smile:

1 Like

Hey @OvermindDL1! Please do tell me more about your ‘more specialized one’. And also, I’m of course going to try to keep the Functional Reactive Programming layer as separate as possible from the WxWidgets layer, so hopefully it will be possible to swap one rendering backend for another.

But this is all still completely theoretical right now, of course :grin:.

Mine just converted atoms to binaries if it compared an atom and binary, just an ease of use thing and I can always convert it before of course. ^.^

1 Like

All right! MapDiff is updated to 1.1.0.
The single but important new feature: Supporting structs in a meaningful way.

  • comparing two structs of the same type returns a map of fields that were changed.
  • comparing two structs of a different type returns a primitive_change where the whole of the old struct was replaced for the whole of the new struct.

Examples:

iex> MapDiff.diff(%Foo{}, %Foo{a: 3})
%{changed: :map_change, struct_name: Foo,
  value: %{a: %{added: 3, changed: :primitive_change, removed: 1},
    b: %{changed: :equal, value: 2}, c: %{changed: :equal, value: 3}}}

When comparing two different kinds of structs, this of course results in a :primitive_change, as they are simply considered primitive data types:

iex> MapDiff.diff(%Foo{}, %Bar{})
%{added: %Bar{a: "foo", b: "bar", z: "baz"}, changed: :primitive_change,
  removed: %Foo{a: 1, b: 2, c: 3}}
5 Likes

By the way,

The library consists of one small module (or actually, one single public function, even).

The code still uses quite a few if/else statements inside this function definition. Is there a way to make this code even more Elixir-idiomatic?

4 Likes

Short circuit the equal case maybe ?

iex(1)> foo = %{ :a => 1, :b => 2 }
%{a: 1, b: 2}
iex(2)> bar = %{ :a => 1, :b => 2 }
%{a: 1, b: 2}
iex(3)> foo == bar
true

Another way to go would be to create a Diff protocol and recursive apply the protocol in it’s implementation.

It’s possible to do same things with functions and pattern matches something like
https://github.com/andre1sk/elixir_map_diff/blob/refactor/lib/map_diff.ex

2 Likes

The equal case is already short-cirquiting, see line 139.

I thought about this, but decided against it, as I believe that the current implementation is the only one that makes sense (different types of structs → primitive change, same types of structs → recurse over struct keys).

This is absolutely amazing. I had no idea that it was possible to refactor it so beautifully :heart_eyes: . Please do open Pull Request, so I can put these changes in and give you credit!

1 Like

Rule of thumbs.

  1. All if’s are case statements

  2. All case statements can ( not should ) be rewritten as pattern matching function heads.

Doing 2 doesn’t always make sense, but in general I find often makes messy code much cleaner.
Sometimes you’ll need to invert the logic ( i.e. it turns out the case statement on the inside should
have been the function call on the outside and the outside logic inside the new function call ).
Not sure that made sense, but I think of it as turning a function inside out.

1 Like

Version 1.2 was just released, which also allows comparisons between non-map (and non-struct) types. It is mostly useful when having a default value such as e.g. nil before you construct a map for the first time.

2 Likes

Didn’t you tell me once that this would be a lot of wasted work because the wxWidgets binding is very unstable, to the point of being almos unusable? Personally I’d love to have a simple wxWidgets binding from Elixir, because some mix tasks are much easier to use from a GUI than from a command line interface.

A Qt binding would be awesome, of course, though. Maybe it’s not thaaat hard to do whatever the Python guys are doing.

Let’s start a new topic if we’re going to talk about WX and QT.

1 Like

I was in a company where we had a huge usage of such a library. It was mostly used for tests.
Basically a test (in erlang) was written:
my_test() ->
Expected = {this, that, [etc]}
Result = module:call(params…)
diff (Expected, Result).

When a test failed you could very quickly see the difference between what was expected and the result. Super usefull

3 Likes