d4mr
How does pattern matching with structs work?
If structs are just maps underneath then how does pattern matching to ensure struct type work?
For example:
defmodule Attendee do
defstruct name: "", paid: false, over_18: true
def may_attend_after_party(attendee = %Attendee{}) do
attendee.paid && attendee.over_18
end
def may_attend_after_party_without_struct(attendee = %{name: "", paid: false, over_18: true}) do
attendee.paid && attendee.over_18
end
end
iex(1)> Attendee.may_attend_after_party(%Attendee{name: "John"})
false
iex(2)> Attendee.may_attend_after_party(%Attendee{name: "John", paid: true})
true
iex(3)> Attendee.may_attend_after_party_without_struct(%Attendee{name: "John", paid: true})
** (FunctionClauseError) no function clause matching in Attendee.may_attend_after_party_without_struct/1
The following arguments were given to Attendee.may_attend_after_party_without_struct/1:
# 1
%Attendee{name: "John", over_18: true, paid: true}
structwtf.exs:8: Attendee.may_attend_after_party_without_struct/1
iex(4)>
%Attendee{} evaluates to %Attendee{name: "", over_18: true, paid: false}, and if structs are just maps underneath, then shouldn’t the behaviour remain similar?
Marked As Solved
ityonemo
Structs have a magic hidden key __struct__ which gets implicitly matched on when you prepend the struct module in between % and {. The default key/values are not filled in for matches, just the struct key.
Also Liked
Eiji
In fact they are! ![]()
iex> Map.put(%{}, :__struct__, Attendee)
%{__struct__: Attendee}
iex> %Attendee{}
%Attendee{name: "", over_18: true, paid: false}
What happen here? First of all the struct is not valid only because “some map” have __struct__ key even if value is valid. The map needs to have all keys available for such struct. The rest is sugar when inspecting.
The %{} syntax or more precisely % special form is just handled differently. Just to imagine:
defmodule Example do
def add(a, b), do: a + b
def add_alt(a, b), do: {:ok, add(a, b)}
end
The only difference between add/2 and add_alt/2 is that first returns just the result and second wraps it in “ok-tuple” which is useful for error handling.
If you check how it’s AST you would see that’s almost the same as in my example:
iex> quote do
%Attendee{name: "", over_18: true, paid: false}
end
{:%, [],
[
{:__aliases__, [alias: false], [:Attendee]},
{:%{}, [], [name: "", over_18: true, paid: false]}
]}
So inside % special form simple map (%{}) is used. The special form simply adds to your map a struct keys with their default values or nil if default value is not specified.
For more information see: %/2 special form
IloSophiep
I’m not certain, but i think the way you pattern match in your function “without struct” is not what you intend to do. What you have is:
def may_attend_after_party_without_struct(attendee = %{name: "", paid: false, over_18: true}) do
That means only maps are “matched”, that
- have an empty name
- did not pay
- and are over 18
I think what you actually want is just matching the existence of the keys, as in the following
def may_attend_after_party_without_struct(attendee = %{name: _, paid: _, over_18: _}) do
attendee.paid && attendee.over_18
end
See how i tell the function head that i want the map to contain those three keys (:name, :paid and :over_18), but i discard the value of those keys - that way i “allow” any value.
Is this what you were hoping for?
IloSophiep
Do you mean those are the default values, if you create an empty Attendee struct? Because as with maps - at least i think - the pattern matching for “apparently empty stuff” is maybe not instantly obvious:
iex(1)> map = %{} = %{a: "foo", b: "bar"}
%{a: "foo", b: "bar"}
iex(2)> map
%{a: "foo", b: "bar"}
As you can see, the “empty map” %{} doe not only match empty maps, but any map. That tends to be very useful for the typical use. But that also means your usage of
def may_attend_after_party(attendee = %Attendee{}) do
does not mean you are trying to match an empty attendee or a default attendee. It means you are matching any attendee struct, no matter the contents.
I hope that was clear - does it make sense to you, the way i described it?
al2o3cr
“Evaluates to” is the gotcha here, the arguments of a function are always a “match context” which has special properties - this:
def some_fun(whole_arg = %{single_value: v}) do
and this:
def some_fun(%{single_value: v} = whole_arg) do
both result in the same function that matches a map with a single_value key and binds whole_arg and v.
This is not the same = that you get when writing those statements alone:
# requires `v` to be defined beforehand, binds a new map `whole_arg`
# always succeeds
whole_arg = %{single_value: v}
# vs
# requires `whole_arg` to be defined beforehand as a map, binds `v`
# fails with MatchError if `whole_arg` does not have a :single_value key
%{single_value: v} = whole_arg
I suspect this is why it’s a common convention to write pattern-matches on structs on the left-hand side:
def may_attend_after_party(%Attendee{} = attendee) do
as it’s clearly distinguishable from the “evaluation” =.








