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?

Last Post!

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

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
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
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New

We're in Beta

About us Mission Statement