d4mr

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

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

Eiji

In fact they are! :smiling_imp:

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

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

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

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” =.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement