Has anyone else ever wanted to merge two maps, but have the resulting map only include keys common to both maps? I think of it as analogous to MapSet.intersection but with a function for handling the values.
m1 = %{a: 1, b:2}
m2 = %{b: 3, c:4}
Map.intersection(m1, m2, fn _k, v1, v2 -> v1 * v2 end)
> %{b: 6}
This would replace
Map.merge(m1, m2, fn _k , v1, v2 -> v1 * v2 end)
|> Enum.filter(fn {k,_v} -> [m1, m2] |> Enum.all?(&Map.has_key?(&1, k)) end)
|> Map.new()
>%{b: 6}
Thoughts?
1 Like
Eiji
January 12, 2023, 5:22am
2
All you have to do is a simple reducer.
defmodule Example do
@spec sample(map(), map(), (Map.key(), Map.value(), Map.value() -> Map.value())) :: map()
def sample(left, right, func) when is_map(left) and is_map(right) and is_function(func, 3) do
Enum.reduce(left, %{}, fn
{key, value}, acc when is_map_key(right, key) ->
Map.put(acc, key, func.(key, value, right[key]))
_pair, acc ->
acc
end)
end
end
iex> Example.sample(%{a: 1, b: 2}, %{b: 3, c: 4}, fn _k, v1, v2 -> v1 * v2 end)
%{b: 6}
Helpful resources:
Guides |> Typespecs @ Elixir documentation
Enum.reduce/3
Kernel.is_function/2
Kernel.is_map/1
Kernel.is_map_key/2
Map.put/3
2 Likes
I think it should be called merge_intersection
or something like that? Union means anything in either of the things.
2 Likes
Of course. Intersection not union. Brain fart
You can do MapSet.intersection
on the keys of both maps and then Map.take
with the result of it.
But I believe @Eiji ’s solution will be more performant.
def merge_intersection(map1, map2, fun) do
for key <- Map.keys(map1) -- Map.keys(map1) -- Map.keys(map2), into: %{} do
{key, fun.(map1[key], map2[key])}
end
end
Sorry, I just hit the wrong “reply” button
adamu
January 12, 2023, 7:09am
8
:maps.intersect(m1, m2)
%{b: 3}
:maps.intersect_with(fn _k , v1, v2 -> v1 * v2 end, m1, m2)
%{b: 6}
8 Likes
I’m assuming that since Elixir’s maps preceded Erlang’s the :maps
module is not simply wrapped by Map
which is why there is no Map.intersect
or Map.intersect_with
. Are Elixir maps completely interoperable with the Erlang :maps
implementation?
There is only ONE type of map in the BEAM. The maps in Elixir are exactly the same maps as in Erlang. Seeing Erlang is the base language on the BEAM Elixir could not have had maps before the BEAM and Erlang implemented them.
iex(1)> m1 = %{a: 1, b: 1, c: 1}
%{a: 1, b: 1, c: 1}
iex(2)> m2 = %{a: 2 , c: 2, d: 2}
%{a: 2, c: 2, d: 2}
iex(3)> mi = :maps.intersect(m1, m2)
%{a: 2, c: 2}
iex(4)> :io.write(m1)
#{a => 1,b => 1,c => 1}:ok
iex(5)> :io.write(m2)
#{a => 2,c => 2,d => 2}:ok
iex(6)> :io.write(m1)
#{a => 1,b => 1,c => 1}:ok
The :io.write
function is an output function in the Erlang io
module and prints the maps which shows they are the same maps as in Erlang.
The :maps.intersect/2
function first came in OTP 24.0 so it may not be used yet in the Map
module.
6 Likes
Thanks for clarifying. I was thinking of MapSets.
Elixir main requires Erlang/OTP 24. Therefore if someone wants to submit a PR that adds intersect/2
and intersect/3
, it will be welcome.
7 Likes
adamu
January 13, 2023, 1:37am
13
Just in case anybody has ideas:
elixir-lang:main
← ed-flanagan:map-intersect
opened 01:30AM - 13 Jan 23 UTC
Following up from https://elixirforum.com/t/map-intersection-2/53121/12. I didn'… t see any duplicates, hopefully it's okay I pushed up.
My first PR, so tried to follow https://github.com/elixir-lang/elixir#contributing & https://github.com/elixir-lang/elixir/blob/main/CODE_OF_CONDUCT.md as best as I could.
---
* Add corresponding functions for Erlang's [`:maps.intersect/2`](https://www.erlang.org/doc/man/maps.html#intersect-2) & [`:maps.intersect_with/3`](https://www.erlang.org/doc/man/maps.html#intersect_with-3).
* Largely cargo-culted from `Map.merge/3`, including the tests & docs. More than happy to extend these.
* I extended the diff list in `KeywordTest` since I wasn't sure if extending `Keyword` would be out-of-scope. Happy to add a `Keyword.intersect` function as well, if desired.
* `make test_stdlib` resulted in `1948 doctests, 4187 tests, 0 failures, 13 excluded`
* Test from running `./bin/iex`
```
❯ ./bin/iex
Erlang/OTP 25 [erts-13.1.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Interactive Elixir (1.15.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Map.intersect(%{a: 1, b: 2}, %{b: 2, c: 3})
%{b: 2}
iex(2)> Map.intersect(%{a: 1, b: 2}, %{b: 2, c: 3}, fn _k, v1, v2 -> v1 + v2 end)
%{b: 4}
```
3 Likes
adamu
January 13, 2023, 1:45am
14
No just misinterpreting from
:sets
until one of the recent OTP versions used a less performant way to maintain sets afaik from before OTP had map support. The updates made where essentially copying what MapSet
s do back to OTP. Building elixir today it probably wouldn’t have MapSet
, but given it exists it could only be removed with 2.0.
https://elixirforum.com/t/native-elixir-module-for-ets/53069/21
So :sets
existed before MapSet
but MapSet
did not build on :sets
, but Map
always build on :maps
?
1 Like
Yes.
MapSet did not build on :sets
because :sets
were not backed by maps but now they are.
1 Like