Sheharyar
Sheharyar

Reputation: 75760

How to check if a Map is also a Struct?

In Elixir I can check if a variable is a map or a struct, by calling Kernel.is_map/1 which makes sense because Structs are Maps underneath, but I'd like to differentiate between the two. I know that I can call __struct__ on a Struct to get it's module name but calling it on a normal map throws:

** (KeyError) key :__struct__ not found in: %{}

So my question is, How do I check if a variable is a map or a struct?


Example use case:

# I want to handle struct and map inputs differently in my Module

defmodule DifferentThings do
  def do_something(arg) when is_map(arg) do
    # Do something with Maps
  end

  def do_something(arg) when is_struct(arg) do
    # But handle Structs differently
    # Issue is, `is_struct` does not exist
  end
end

Upvotes: 17

Views: 8999

Answers (6)

ottobar
ottobar

Reputation: 4361

As Sheharyar noted, Elixir 1.10.0 introduces the is_struct/1 guard. Therfore, your original code will work.

Upvotes: 3

Sheharyar
Sheharyar

Reputation: 75760

While the simplest way to check if a term is a map is to use:

Map.has_key?(map, :__struct__)

It does not work in a guard so we had to previously resort to pattern match the :__struct__ key in the function clause:

def some_fun(%__struct__: _module}), do: ...

But this just plain looks dirty.


Guard: is_struct/1

Starting in Erlang/OTP 21, a couple of new guards were introduced that we can use to define our own:

defguard is_struct(term) when is_map(term) and :erlang.is_map_key(:__struct__, term)
defguard is_struct(term, module) when is_struct(term) and :erlang.map_get(:__struct__, term) == module 

Now you can use them as function guards:

def do_something(account) when is_struct(account, User) do
  # Do something with %User{} structs
end

def do_something(struct) when is_struct(struct) do
  # Do something with random structs
end

def do_something(map) when is_map(map) do
  # Do something with maps
end

Note: These will soon be added to Elixir standard library itself. Follow the discussion in this ElixirForum thread.

Upvotes: 5

Rajeesh
Rajeesh

Reputation: 4485

It is possible to pattern match between struct and map like below

defmodule DifferentThings do
  def do_something(arg = %_x{}) do
    IO.puts "This is a struct"
  end

  def do_something(arg = %{}) do
    IO.puts "This is a map"
  end
end

Upvotes: 11

Oleksandr Avoiants
Oleksandr Avoiants

Reputation: 1929

In general to check if map is a struct:

Map.has_key?(struct, :__struct__)

For different method declarations (more general method is second):

defmodule DifferentThings do
  def do_something(%{__struct__: _} = arg) do
    # ...
  end

  def do_something(arg) when is_map(arg) do
    # ...
  end
end

Upvotes: 28

You can't have separate function heads for Map vs Struct with a guard, but you can do it with pattern matching.

defmodule Guard do

  def foo(%{:__struct__ => x })  do
    Struct
  end

  def foo(x) when is_map x do
    Map
  end

end

Upvotes: 5

PatNowak
PatNowak

Reputation: 5812

You can check easily keys of current map with Map.keys/1.

For both map and struct is_map/1 will return true, but in your example:

Map.keys(%{}) will return []

and

Map.keys(struct) 

will return collection of keys, eg. [:__struct__, :name, :age].

So you can simply use:

:__struct__ in Map.keys(struct).

If you want to have this is_struct make it macro then.

Upvotes: 4

Related Questions