Given `a = pattern`. `match?(^a, b)` returns false. `^a = b` returns MatchError. But `pattern = b` matches. Why?

My use case is I want to dynamically match a struct based on the struct type, and a key in the struct.


defmodule Thing do
  defstruct [:id, :name]

things = [
  %Thing{id: 1, name: "thing1"},
  %Thing{id: 2, name: "thing2"},
  # ..other stuff, could be non-Thing structs..

type = Thing
id = 2

match = %{__struct__: type, id: id}

Enum.find(things, fn thing -> match?(^match, thing) end)
# => nil

match?/2 returns false here:

iex(1)> match
%{__struct__: Thing, id: 2}
iex(2)> match?(^match, %Thing{id: 2, name: "thing2"})
iex(3)> %{__struct__: Thing, id: 2} = %Thing{id: 2, name: "thing2"}
%Thing{id: 2, name: "thing2"}

Or with a case statement:

case %Thing{id: 2, name: "thing2"} do
  %{__struct__: Thing, id: 2} -> :ok
  _ -> :not_ok
=> :ok

Now if we assign the pattern to a variable with match = %{__struct__: Thing, id: 2}, it no longer works.

case %Thing{id: 2, name: "thing2"} do
  ^match -> :ok
  _ -> :not_ok
=> :not_ok

I’m curious 1. why does behave this way? 2. What is the right way to achieve what I’m trying to achieve. Assuming that the list could be mixed structs.

Patterns cannot be constructed at runtime or assigned a variable. You can kinda see that reflected in the fact that match? is a macro and not simply a function. Using a pin will turn the comparison between the pinned variable and the part of the right side value to match into an eqality comparison (==) not a match by „pattern“. So your only option here would be explicitly coding matches per type and selecting only the code path at runtime.


^match will compare the two values for equality. Your match variable is an incomplete struct without the :name property, so not equal to your proper Thing struct with id: 2.


And btw, you don’t need __struct__ field to match on type of a struct. You can just do

%type{} = %SomeStruct{}
IO.inspect(type) # prints SomeStruct

and type will contain the type of a struct.