Can a defguard definition match against a struct?

Hello,
Is it possible to check if an argument to a guard is a particular structure?

Context: I am trying to have a defguard definition that checks if something is an Ecto query – which means it can either be a module (e.g. atom) or it can be an %Ecto.Query{}. But I cannot seem to find a way to do it via defguard.

defmodule A do
  defguard is_query(q) when is_atom(q) or ... # <- what do we put here?
end

Tried with match?(%Ecto.Query{}, q) but that did not compile.

Something I am missing?

:wave:

Just in case, Ecto Queryable is not limited to modules and queries, but can be anything it’s implemented for …

Maybe you could use Ecto.Queryable.impl_for(term)? It would at least be accurate.

How would you use it in a defguard?

cannot invoke remote function Ecto.Queryable.impl_for/1 inside guards

As for your actual question:

defguard is_query(a) when is_map(a) and :erlang.map_get(:__struct__, a) == Ecto.Query
1 Like

Why would you need a guard if you have impl_for? It serves the same function – checking whether something is a queryable, – as far as I understand your goal.

Well, I am trying to find a way to introduce a stricter typing when feeding parameters to my functions.

Ecto would raise if it receives something that doesn’t implement Ecto.Queryable … So is it really important to have it fail at the guard to your function instead of at some check within ecto?

I feel it is but having no choice, I’ll accept the raise way of doing things by Ecto.

This works on the surface but as you said, it will not capture any other implementations of the Ecto.Queryable, correct?

it will not capture any other implementations of the Ecto.Queryable , correct?

Right, it wouldn’t …

Had to be sure. Thanks for the help! :slight_smile:

Does this mean that it will not capture dynamics/composed queries?

The simpler way to do this now is defguard is_query(a) when is_struct(a, Ecto.Query) using Kernel.is_struct/2, which was introduced after this thread was last active. PR #9470

As for your question, I am not 100% sure, but if what you have created is an Ecto.Query struct, it will match

3 Likes

Ha, I like this a lot. Thanks for pointing it out.

1 Like

It is impossible to do in a defguard. I think it is possible, with big caveats so maybe actually impossible too, with a defmacro. If the protocol is consolidated, you can grab the list of modules implementing it. So if your protocol is consolidated by the time you call your guard, which generally may not necessarily be the case, that should work. Here’s a sketch:

defmodule Macros do
  defmacro is_impl(term, protocol) do
    protocol = Macro.expand(protocol, __ENV__)

    impls =
      case protocol.__protocol__(:impls) do
        {:consolidated, impls} ->
          impls

        :not_consolidated ->
          raise "#{inspect(protocol)} is not consolidated"
      end

    quote do
      unquote(term).__struct__ in unquote(impls)
    end
  end
end

defmodule Foo do
  import Macros

  def f(enumerable) when is_impl(enumerable, Enumerable) do
    Enum.to_list(enumerable)
  end
end

besides the caveat around consolidation time, in the list of impls we have things like Atom, Integer, …, Any, and they all would have to be handled too.

So yeah, not worth it. :slight_smile:

1 Like