How can you assert that a map in a variable matches another map that is a superset?

pattern-matching
testing

#1

Given these two maps:

map1 = %{a: 1}
map2 = %{a:1, b: 2}

This works fine:

%{a: 1} = map2

But these do not:

iex> ^map1 = map2
** (MatchError) no match of right hand side value: %{a: 1, b: 2}
iex> match?(^map1, map2)
false

Assuming I have a subset of what I want to match in a variable is there a way to use pattern matching to check that I have a subset?


#2

The trivial/pedestrian (i.e. no pattern match) way:

map1 = %{a: 1}
map2 = %{a: 1, b: 2}

set1 = MapSet.new(map1)
set2 = MapSet.new(map2)

IO.puts("map1 ⊆ map2 #{MapSet.subset?(set1, set2)}")
IO.puts("map2 ⊆ map1 #{MapSet.subset?(set2, set1)}
$ elixir demo.exs
map1 ⊆ map2 true
map2 ⊆ map1 false
$ 

or

map1 = %{a: 1}
map2 = %{a: 1, b: 2}
map3 = %{c: 3}

subset? =
  fn (map1, map2) ->
    keys = Map.keys(map1)
    map2
    |> Map.take(keys)
    |> Map.equal?(map1)
  end

IO.puts("map1 ⊆ map2 #{subset?.(map1, map2)}")
IO.puts("map2 ⊆ map1 #{subset?.(map2, map1)}")
IO.puts("map3 ⊆ map2 #{subset?.(map3, map2)}")
$ elixir demo2.exs
map1 ⊆ map2 true
map2 ⊆ map1 false
map3 ⊆ map2 false

and another

map1 = %{a: 1}
map2 = %{a: 1, b: 2}
map3 = %{c: 3}

subset? =
  fn (map1, map2) ->
    in_other =
      fn {key, value} ->
        {:ok, value} == Map.fetch(map2, key)
      end
    map1
    |> Map.to_list()
    |> Enum.all?(in_other)
  end

IO.puts("map1 ⊆ map2 #{subset?.(map1, map2)}")
IO.puts("map2 ⊆ map1 #{subset?.(map2, map1)}")
IO.puts("map3 ⊆ map2 #{subset?.(map3, map2)}")
$ elixir demo3.exs
map1 ⊆ map2 true
map2 ⊆ map1 false
map3 ⊆ map2 false
$ 

I have the sneaking suspicion that the pattern is a compile time construct - it’s the binding that happens at runtime.


#3

Do not pin here:

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

#4

this doesn’t do what you would think, though:

iex(4)> match?(a, %{x: 5})
true

#5

Oh… Yeah, true, thats not what I had expected…


#6

match?(a, %{x: 5})

That simply results in %{x: 5} being bound to a - i.e. it is a match that always succeeds, while a is subsequently never used (macro code from Kernel.match?/2).

Similar to:

a = %{a: 1}

match1 =
  case %{x: 5} do
    a -> true   # i.e. "%{x: 5}" is bound to "a" - a subsequently unused name
    _ -> false
  end

# Semantically similar to above case using a plain match/bind.
match2 =
  try do
    a = %{x: 5} # i.e. "%{x: 5}" is bound to "a" - a subsequently unused name
    true

  catch
    :error, {:badmatch, _} ->
      false

#  # --- rescue is PREFERRED in Elixir
#  rescue
#    _ in MatchError ->
#      false
  end

# https://elixir-lang.org/getting-started/try-catch-and-rescue.html#errors
#
# In Elixir, we avoid using try/rescue because WE DON’T USE ERRORS FOR CONTROL FLOW.

IO.puts("match?(a,{x: 5}) #{match1}")
IO.puts("alt_match?(a,{x: 5}) #{match2}")
$ elixir demo4.exs
warning: variable "a" is unused
  demo4.exs:1

warning: variable "a" is unused
  demo4.exs:5

warning: variable "a" is unused
  demo4.exs:12

match?(a,{x: 5}) true
alt_match?(a,{x: 5}) true
$

#7

that’t my point :wink:


#8

I would go with @peerreynders suggestion using MapSet. Or maybe:

map1 = %{a: 1}
map2 = %{a: 1, b: 2}

Map.take(map2, Map.keys(map1)) == map1

But I guess any of @peerreynders suggestions have better performance, memory consumption and are clearer. So no reason to use mine. :wink:


#9

Thanks! It looks like using MapSet.subset?/2 is my best option


#10

Now I see my code is actually the same as @peerreynders second suggestion but in only one line.

If you want also, his last suggestion can be written as:

map1 = %{a: 1}
map2 = %{a: 1, b: 2}

map1
|> Map.to_list()
|> Enum.all?(&(&1 in map2))

But I still think MapSet.subset?/2 is clearer, and as you can see in its code, looks like it does pretty much the same thing.


#11

Now that @axelson has a workaround for it, I want to bring a little discussion: shouldn’t his example work? I mean this one:

map1 = %{a: 1}
map2 = %{a: 1, b: 2}

^map1 = map2

I totally expected it to work. Didn’t you?​

I mean, if we think logically: if %{a: 1} = b works and
a = %{a: 1}, then ^a = b should work.

Do any of you know why it doesn’t? Is there a specific reason for it?


#12

Pins always operate by value. Pinning a value does not turn the value into a pattern.


#13

My gut pick is this one:

subset? =
  fn (map1, map2) ->
    in_other =
      fn {key, value} ->
        {:ok, value} == Map.fetch(map2, key)
      end
    map1
    |> Map.to_list()
    |> Enum.all?(in_other)
  end

as it simply verifies that all the necessary key/value pairs are present and will stop as soon as that is not the case.

In this case I also prefer to use fetch/2 in order the leverage the underlying map functionality rather than relying on in/2's macro magic even if it makes the code a bit more verbose (so be it).

One thing to be aware of is that the empty set is a subset of any other set - the same is true for empty maps.


#14

http://erlang.org/doc/reference_manual/patterns.html

In a pattern matching, a left-hand side pattern is matched against a right-hand side term.

The phrasing suggests to me that patterns are very different from terms - patterns aren’t listed under Data Types.

Pinning ‘pins’ the binding - it expresses that no re-binding of that name should take place; at that point matching succeeds if both values are equal otherwise there will be a :badmatch error.

Erlang doesn’t need pinning because it doesn’t allow re-binding. But a simple name to the left of the match operator (=) means:

  • if the name is unbound, bind the name to the right hand side (and return the bound/matched value)
  • if the name is bound and the name is bound to a value equal to the right hand side, succeed (and return the matched value)
  • if the name is bound and the name is bound to a value not equal to the right hand side, fail (and throw a badmatch error)

FYI: Map is an Enumerable so Enum.all?(map1, &(&1 in map2)) should work - I just like to be explicit.


Pattern matching literals vs bounded variables
#15

I see. Maybe the variable rebinding ability makes this a little confusing behaviour in Elixir.

You’re right, and I’ve noticed that you’re very explicit. That’s good, even more in examples, but I just wanted to show him he can write it in a shorter way. :slight_smile:


#16

@peerreynders Do you like that version mainly for performance over the simpler:

MapSet.subset?(MapSet.new(map1), MapSet.new(map2))

I wonder if there’s a way to convert a map to a pattern. Conceptually it seems possible.


#17

Either would be hidden behind a function so concise/simple/explicit/verbose really doesn’t factor into it. I would hope that the fetch solution is pretty good performance/memory wise - but that could be totally wrong - one would have to benchmark the various solutions in a representative environment to actually know.

Conceptually it seems possible.

At this point I doubt it. I can’t find a source but (at least for me) everything seems to hint that patterns are fixed at compile time - so short of writing a module with the injected pattern and compiling and then loading it…

It may have been one of those tradeoffs that allowed the BEAM to heavily optimize pattern matching - but I’m just guessing.


#18

Yeah it would make sense if a pattern is a compile-time construct in which case there would be no good way to use pattern matching for my problem as stated. Although for my current problem I am creating my map statically at compile time so I may be able to send my map to a macro and use that macro to pattern match against a map.


#19

Crudely (… must be one of the simplest applications for macros I’ve come across):

defmodule Pattern do
  defmacro from(pattern) do
    quote do
      unquote(pattern)
    end
  end
end

defmodule Demo do
  require Pattern
  @demo_map %{a: 1}

  def subset?(super_map) do
    case super_map do
      Pattern.from(@demo_map) ->
        true
      _ ->
        false
    end
  end

  def subset2?(super_map) do
    try do
      Pattern.from(@demo_map) = super_map
      true
    catch
      :error, {:badmatch, _} ->
        false
    end
  end

  def subset3?(Pattern.from(@demo_map) = _super_map), do: true
  def subset3?(_), do: false

  def a,
    do: @demo_map

  def with_fun(subset) do
    fn {k,v} ->
      IO.puts("a ⊆ #{Atom.to_string(k)}: #{subset.(v)}")
    end
  end

  def with_fun({title, list}, fun) do
    IO.puts(title)
    Enum.each(list, with_fun(fun))
  end
end

check_list = [a: Demo.a, b: %{a: 1, b: 2}, c: %{c: 3}, e: %{}]

Demo.with_fun({"--- subset? ", check_list}, &Demo.subset?/1)
Demo.with_fun({"--- subset2? ", check_list}, &Demo.subset2?/1)
Demo.with_fun({"--- subset3? ", check_list}, &Demo.subset3?/1)
$ elixir demo5.exs
--- subset? 
a ⊆ a: true
a ⊆ b: true
a ⊆ c: false
a ⊆ e: false
--- subset2? 
a ⊆ a: true
a ⊆ b: true
a ⊆ c: false
a ⊆ e: false
--- subset3? 
a ⊆ a: true
a ⊆ b: true
a ⊆ c: false
a ⊆ e: false

#20

@demo_map is replaced literally with %{a: 1} before the macro is called.

In theory it should be possible to use @demo_map directly at the exact same places where you currently use the macro.