Pattern matching params

Hello!

I have a question about a better way to do something like the following:

 def index(conn, params = %{"AccountSid"=> account_sid,
                    "ApiVersion"=> api_version,
                          "Body"=> body,
                          "From"=> from,
                      "FromCity"=> from_city,
                   "FromCountry"=> from_country,
                     "FromState"=> from_state,
                       "FromZip"=> from_zip}) do
...
end

Say you have a controller action that you’re pattern matching a lot of params against, is there a better way to do it than what’s show above?

Thanks!

Binh

Just match on params and use Access behaviour and syntax:

This will have the culprit though, that your action is called even when not all params are filled in and you have to manually check in the functions body. So in my opinion, matching them all in the head is the better way.

iex(1)> foo = %{"bar" => 0, "baz" => 1}
%{"bar" => 0, "baz" => 1}
iex(2)> foo["bar"]
0
iex(3)> foo["baz"]
1
iex(4)> foo["quux"]
nil
2 Likes

You can match them in plugs and accumulate the matches in assigns or private.

plug :extract_api_version
plug :extract_account_sid
plug :extract_from_fields

def index(%Plug.Conn{assigns: assigns} = conn, %{"body" => body}) do
  # assigns now supposedly contain :from, :api_version, :account_sid
  # ...
end

@spec extract_api_version(Plug.Conn,t, Plug.opts) :: Plug.Conn.t
defp extract_api_version(%Plug.Conn{params: %{"api_version" => api_version}} = conn, _opts) do
  if correct?(api_version) do
    assign(conn, :api_version, parse(api_version))
  else
    conn
    |> put_status(:bad_request) # 400
    |> halt()
  end
end

# etc

If by better way you mean moving that large pattern away, so your function head is cleaner, you could use expat, just released it’s v1.0 today.

2 Likes

Just wanted to add an example on how expat could help in situations like yours - matching big structures, sorry if that’s not what you meant with better way.

use Expat

# You might want to split the pattern into the
# pieces that fit your domain.
# In these examples `m_` stands for Map

defpat m_auth(%{"AccountSid"=> account_sid,
                    "ApiVersion"=> api_version})

defpat m_from(%{"From"=> from,
                     "FromCity"=> from_city,
                     "FromCountry"=> from_country,
                     "FromState"=> from_state,
                     "FromZip"=> from_zip})

defpat m_body(%{"Body" => body))

defpat index_params(m_auth() = m_from() = m_body())

# The following will expand to your whole pattern,
# as you had in your original code, also if any
# named pattern had a guard, it would be expanded
# properly as part of `def index`. 
def index(conn, params = index_params()) do
  # Code here will only be executed if the params
  # matched your whole pattern. So it's safe.
  # You can also use your patterns in here
  # in order to extract some values you are 
  # interested to work with:
  m_from(from_country: country) = params
  IO.puts("Got request from country #{country}")
  ...
end
3 Likes

In the end, having those patterns in your function head is a very explicit thing to do.

I would only consider moving them elsewhere if there is a very clear amount of duplication going on.

The expat library looks nice, but I would be a little cauteous not to introduce too much magic to my functions.

4 Likes

Yep, duplication is solved by a magic called abstraction, that’s why we have functions.

Thank you everyone for your suggestions.

Still new to Elixir so it’s gonna take me the weekend to evaluate all of these suggestions and see if it fits with our current understanding of the language and framework.

I do like the explicitness of the massive map, but it’s just unsightly. And Expat looks really useful.

I like the way it’s done in the Elixir codebase itself, for instance:

Stuff that really needs to be matched in the function head (due to overloads) is matched in the head; other stuff is matched on a separate line. If it gets too long you can just use two or three lines.

As a plus the error message when there’s a match error is easier to follow with this approach.

2 Likes