d4mr

d4mr

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?

Showing Posts 1 to 8

al2o3cr

al2o3cr

Your example code omits the definition of Attendee.may_attend_after_party_without_struct/1; I tried def may_attend_after_party_without_struct(attendee = %{}) do and it worked, so you’ll need to provide more information about what behavior you’re expecting.

d4mr

d4mr OP

My bad, updated the example

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.

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?

d4mr

d4mr OP

I understand your pattern matching snippet, and in fact that is what I would expect to work.

However, because structs are matched in this manner:

def may_attend_after_party(attendee = %Attendee{}) do
    attendee.paid && attendee.over_18
end

and because %Attendee{} evaluates to %Attendee{name: "", over_18: true, paid: false} I expected the other behaviour to work.

However @ityonemo has given a very concise answer about why this works and @Eiji has provided an amazing demonstration of the fact.

Thanks a lot! This was my first interaction with the Elixir community, and makes me very happy to be a part of it :smile:

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

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews