Refactor a pattern match

i have a controller that looks

defmodule Bonsai.LedgerController do
  use Bonsai.Web, :controller

  alias Bonsai.{Repo, Ledger, Income, Expense}

  plug Guardian.Plug.EnsureAuthenticated, handler: Bonsai.SessionController
  plug :scrub_params, "ledger" when action in [:create]

  def index(conn, _params) do
    ledgers = Ledger.all(conn.assigns[:organization_id])

    render(conn, "index.json", ledgers: ledgers)
  end

  # Create an income
  def create(conn, %{"ledger" => %{"type" => "income"}} = ledger_params) do
    case Income.create(get_params(conn, ledger_params["ledger"]), user_id(conn)) do
      {:ok, ledger} ->
        conn
        |> put_status(:created)
        |> render("show.json", ledger: ledger)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render("error.json", errors: changeset.errors)
    end
  end
end

I’m using pattern match for the ledger_params but I have to use Income.create(get_params(conn, ledger_params["ledger"]) nested, in most cases when one does not pattern match the params would look %{"amount" => 10, "contact" => "Boris"}, but in this case it looks like %{"ledger" => %{"amount" => 10, "contact" => "Boris"}} is there a way to make it the params map wihout the nesting.

1 Like

If I understood correctly, this is what you’re looking for:

def create(conn, %{"ledger" => %{"type" => "income"} = ledger_params}) do
1 Like

And, if you dislike Yoda matching, you can also write this as

    def create(conn, %{"ledger" => ledger_params = %{"type" => "income"}}) do

2 Likes

Yeah that is what I wanted, thanks

1 Like

Took me a while to track that back to Yoda Conditions[1]. But it does bring up an interesting point: Why are both ways even allowed? In this particular case to me

%{"ledger" => %{"type" => "income"} = ledger_params}

seems clearer because elements pertinent to “the pattern” seem to be neatly on the left side while what seems to be an alias/variable is out of the way on the right. In

%{"ledger" => ledger_params = %{"type" => "income"}}

ledger_params seems to disrupt the flow of the pertinent pattern elements - but strictly speaking to me this should be the more correct way.

In the phoenix framework guide:

def show(conn, %{"messenger" => messenger} = params) do
  ...
end

It’s good to remember that the keys to the params map will always be strings, and that the equals sign does not represent assignment, but is instead a pattern match assertion.

Meanwhile in the Elixir documentation:

A variable can only be assigned on the left side of =:

In the above examples the “variable” seems to be quite happy on the right side of the = match operator. So the rules seem to be far more nuanced:

  • Outside of a pattern, the pattern itself and any match variables have to be on the left side of =.
  • Inside of a pattern, match variables can occur of either side of the = match operator. For parameter level matching the pattern always extends over the entire parameter so that variables can be on either side of the = match operator.

I.e. both

def show(conn, %{"messenger" => messenger} = params) do   
    ...
end

and

def show(conn, params = %{"messenger" => messenger}) do   
    ...
end

seem to be valid.

I’m just looking for some insight so that I can adjust my mental model to make sense of this again.

[1] Used the tactic a lot way before 2010 in C/C++ coding - only ran into the term later because of JS linters - and promptly forgot about it.

2 Likes

If you look at the projects under the elixir-lang org on GitHub and at Phoenix (as a fairly sizeable project), you’ll see that putting the var on the right is the prevalent style.

I personally prefer it because the pattern is the more important part of the function head – it directly influences the choice of the function clause that is going to be invoked for a given call. The variable name becomes relevant inside the function body, i.e. after I’ve visually parsed the function head and decided that this is the clause I want to dive into.

In order to more easily remember the difference between the “assignment”-matching and the function clause matching, think about the former as matching from right to left, e.g.

x <- <pattern>

whereas when a function clause is matched, the arguments are coming from the top:

     arg1                 arg2
      ||                   ||
      \/                   \/
     +--+  +----------------------------------+
     |  |  |                                  |
show(conn, %{"messenger" => messenger} = params) do

With this mental model it is clear that it doesn’t matter whether it is <pattern> = params or params = <pattern>, the end result is going to be the same.

Do keep in mind though, this last rule also has effect in nested matches that are not necessarily part of a function head, e.g.

{"f" <> _ = x, _} = {"foo", "bar"}
{x = "f" <> _, _} = {"foo", "bar"}
3 Likes

Both ways, pattern = var and var = pattern, are allowed because of what a = in a pattern really means. It is an alias and actually means that both sides much match, so the LHS of = must match and the RHS as well. You could actually write a pattern (x,y,z} = {a,b,c} which is perfectly legal but not really very useful.

The compiler does some checking that to ensure that it is possible for both sides to match, so if you try:

def foo({a,b,c} = {x,y} do ... end

then the compiler will complain. You can use = anywhere in a pattern not just at the top-level, for example [x|[y|z]=tail].

I personally prefer pattern = var in a pattern because that to me much more says a pattern then the other way around.

Robert

3 Likes

@alco: Thank you for your explanation on right-to-left vs top-down.
And @peerreynders: Very nice post!

It seems that putting the name at the right vs the left of the = is just a matter of personal preference.

Why to put it at the left:

  • It makes it ‘less hard to read’ because the bind order is similar as to what it is outside of matches.
  • As the names are to the left of the =, you never need to worry with the pin (^) operator (of course, In function clauses, this does not apply anyway)
  • It is more natural for people coming from languages like Haskell, which has the assigns-@ syntax: list@(head:tail) which would translate to list = [head | tail] in Elixir.

Why to put it at the right:

  • It makes the pattern itself more readable as it comes first. (As long as the pattern is no more than two levels deep, of course)
  • Multiple large projects, such as elixir-lang.org and Phoenix, have chosen to adopt this style.

In the end, I think this might be a lot like little-endian vs big-endian, or (dare I say it?) spaces vs tabs. :smile:


@rvirding: def foo(a = b = c = d = e = f = g = h), do: [a,b,c,d,e,f, g, h] :open_mouth:

2 Likes

@alco, @rvirding, and @Qqwy - thank you for your great explanations and clarifications. It seems to boil down to:

  • The Elixir’s “Getting Started” documentation assertion that a “variable can only be assigned on the left side of =” refers to the case where the match operator is used to initiate a pattern match. Inside the pattern the match operator works both ways.
  • The implicit top-down pattern match visualization for function parameters is a great reminder that the explicit portion of the argument is the pattern.

As long as “personal preference” isn’t synonymous to “completely arbitrary” :grin: - but as you pointed out reasonable arguments can be made for either way depending on the circumstances.

1 Like