Why was equal sign used in with statement?

I read an article on the Dockyard blog and I’m wondering why they used an equal sign with a with statement.

The code example is:

defmodule CMS.PageController do
  
  def update(conn, %{"id" => id, "page" => page_params}, current_user) do
    with page = CMS.get_page!(id),
         :ok <- Authorizer.authorize(:update, page, current_user),
         {:ok, page} <- CMS.update_page(page, page_params) do
    
      conn
      |> put_flash(:info, "Page Updated")
      |> redirect(to: cms_page_path(conn, :show, page)
    end
  end
end

Why was page = CMS.get_page!(id) used instead of <-?

1 Like

with is a macro that evaluates a set of expressions and the <- symbol is a hint to that macro that the right-hand side will either match the left, bind any unbound variables, and then continue to the next expression, or it won’t match, and it will follow the else clause matching if present, or just return the unmatched clause. But if you don’t use the <- then it’s just a typical expression, and it will run it, and do the next thing.

Using a = inside a with is usually a hint that either the expression will always pass (e.g. string = "asdf:" <> some_known_value) or that you want it to raise an error if it something is wrong. In this case because the right side of the = is CMS.get_page!/1 and ! is typically used when you want to raise if something isn’t found, I’d expect that get_page!/1 will raise an error if it doesn’t find the page, so we can assume that page = ... will always pass (if it doesn’t raise an error). If it didn’t raise an error and returned {:ok, page} but the author wanted it to raise in that scenario they could change it to {:ok, page} = CMS.get_page(id),

5 Likes

I think it should be:

with {:ok, page} <- CMS.get_page(id), 
     :ok <- Authorizer.authorize(:update, page, current_user),
     {:ok, page} <- CMS.update_page(page, page_params) do
...

or

page = CMS.get_page!(id)

with :ok <- Authorizer.authorize(:update, page, current_user),
     {:ok, page} <- CMS.update_page(page, page_params) do 
...

The code with page = CMS.get_page!(id) raises in case of error even if it would be outside of with, so it has no reason to be placed inside that with clause (it does not need to be inside that with).

2 Likes

In this case, with being the only block in the function it does not matter, but still its a good thing to keep the scope of a variable as small as possible.

1 Like

In this case error’s message will change also (pattern match error). This way phoenix cannot handle this error as “not found” (404) in production if debug_errors: false, it will be 500 error.

1 Like

Generally my expectation when I see get_… is that it will return the thing or nil or a provided default value. If you want to return {:ok, thing} or an error fetch_… would be a more consistent naming.

In regards to the original question, I use = in with clauses to indicate that I’m not matching on that result. Sometimes you may need to do something between matches in a with that you don’t need to match on (like creating a changeset) and = signals intent better.

1 Like

Sorry for asking, would you mind showing me an example?

I keep re-reading your answer and it’s not sticking.

By raising (assuming that ! does as intended) the error will fall through the with clause, and does not appear to be handled. I’m going to assume that’s a mistype :thinking:

What would be the difference if we use page <- CMS.get_page!/1? It would be the same wouldn’t it? I can’t think of anything subtle being said by using the =. If I were to see page <- CMS.get_page/1 (notice no !) I would also assume “This always matches”.

It’s probably not a mistype at all. You don’t need to handle everything in a with, and in their case they’re not handling anything in with.

Since they’re not handling anything in a with everything is falling through, whether it falls through as a throw or as a returned error, and my educated guess is that since we’re looking at a controller they’re:

  1. Relying on the fallback controller they would no doubt register with action_fallback to handle the return value of failing to match authorize or update_page
  2. Relying on Plug.Exception's coercion of Ecto.NoResultsError (as one would expect get_page! to raise) to turn that error into a 404 response

So they are using = because they aren’t trying to match on the value, they just expect it to work or raise.

It’s contrived, but

with :ok <- validate(params),
     changeset = Thing.create_changeset(%Thing{}, params),
     {:ok, thing} <- Repo.insert(changeset) do
  Logger.info("I created thing #{thing.id}")
  {:ok, thing}
end

When I created the changeset, it’s gonna work no matter what, so I use = instead of <- because there’s nothing to match on.

In your original question they did they same because they don’t care what get! returns because either it’ll return what they want or it’ll raise.

2 Likes

Thank you! I think I got the intent :clap: Your example is more clear to me than the blog post.

Things that got me confused:

  1. Errors falling through with. Then why use with?
  2. The first condition in the with is page = CMS.get_page!(id), but if we don’t care about matching, why even put it in the with, it could well be outside, especially if it raises, which would make it even more clear. The with (to me) for get_page!/1 is a bit of indirection.
  3. The scope of page is then used in the redirect, how? (is that a mistype?)

In any case, your example of the changeset in the middle of the with is more understandable to me. I wouldn’t want to execute the create_changeset unless the :ok prior succeeded. Also, changeset is likely not fail, and I don’t want to use it for matching anyway (since it would be used in the else clause.
(I don’t think I’ve explained it well)

Thank you again.

1 Like

@bennelsonweiss explained it perfectly.

  1. They use with because they want to handle those errors by matching on them in action_fallback like:
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(MyAppWeb.ChangesetView)
    |> render("error.json", changeset: changeset)
end

def call(conn, {:error, something}), do: ...

But Ecto.NoResultsError that can be raised by get_page!, handled by Plug.Exception implicitly and then may be directed to action_fallback, but not in dev environment, because in dev we may want to debug those errors earlier.

  1. You are right! It should be placed outside in this example, if you don’t care about binding scope; as @Sebb said he would limit the scope of page binding, but in this example it does not matter.

  2. cms_page_path(conn, :show, page) just works because it sees page binding, there is no inner scope between with ... do, only between with ... end, if I understood your question.

everywhere = 1

with only_inside_a = 2,
     only_inside_b = 3 do
  IO.puts(everywhere) # 1
  IO.puts(only_inside_a) # 2
  IO.puts(only_inside_b) # 3
end

IO.puts(everywhere) # 1
IO.puts(only_inside_a) # ERROR!
IO.puts(only_inside_b) # ERROR!

The simplest reason is that it lets you define a clear sequence of execution for functions that may fail without needing to nest conditional checks.

In the example you’re fetching a page (page may not exists), then authorizing the current user to interact with that page (user may not be authorized), then updating the page (update may be invalid).

You could nest three ifs, “if page is not found fail, else if user is not authorized fail, else if page cannot be updated fail, else put flash and redirect”, but then all the error handling obscures what you’re actually trying to achieve.

with allows you to structure it as “successfully get the page, then ensure the user is authorized, then update the page, then put flash and redirect”. That way the happy path is clear to see, but you haven’t ignored the fact that things can go wrong because you’re still checking for that, just not handling it right there.

You could just as easily not put it in the with, but there’s a non-technical reason you might want to put it in there.

Since the with is defining the logic for some process, in this case how you update a page, and getting a page is a fundamental part of the process as they’re defining it, then including it in the with puts all the steps of the process at the same level so that it’s clear to understand each stage of the process.

It’s a decision entirely aimed towards trying to present something clearly for the developer, and it might not always be the clearest choice given a team’s conventions.

Everything defined between with and do is available in the do block (but not in the else block, if there is one), and if you reach the do block it means there were no errors before.

2 Likes

Thank you everyone!!! :clap: @bennelsonweiss @focused @Sebb @felix-starman

Such a nice implementation in the original example :sweat_smile: :blush:

3 Likes