Why does only one of these 'for's work?

I was playing around with for this morning, iterating over a list of maps:

errors = [
  %{title: ["should be at least 3 characters"]},
  %{description: ["must provide a description", "another issue"]}
]

It’s cool that you can treat for almost like a pipeline, to loop over each one of the above errors and then loop over the lists inside of them. But I’m curious why this works:

for errors <- errors, {_, error_list} <- errors, err <- error_list do
   IO.inspect(err)
end

but this does not:

for {_, error_list} <- errors, err <- error_list do
   IO.inspect(err)
end

To my thinking the first error <- errors is redundant?

1 Like
for error_map <- errors,
    {_key, error_list} <- error_map,
    err_string <- error_list do
  IO.inspect(err_string)
end

maybe this makes it more clear?

Edit: I notice now the data format is a little weird. You use one map per key, which is unnecessary and causing the code to be less clear. The errors could be in a single map/keyword e.g.

errors = %{
  title: ["should be at least 3 characters"],
  description: ["must provide a description", "another issue"]
}

which would allow you to use the simpler two-step for that currently doesn’t work

2 Likes

Thanks @APB9785, I think I spent too long looking at this, yeah I see how error_map <- errors is working in the little “pipeline”.

Maybe I overthought this, that errors is what I assumed the output of Ecto.Changeset.traverse_errors/2 would be based on their docs. But I haven’t tested this with multiple errors on a changeset so maybe my input would look different?

 [
    email: {"can't be blank", [validation: :required]},
    firstname: {"can't be blank", [validation: :required]},
    lastname: {"can't be blank", [validation: :required]}
  ]

The above discussed for works with this but now that I’ve released it’s a keyword list I think I’ll find a better way of handling this.

1 Like

It’s important to understand that a keyword list is syntactic sugar for a list of 2-element tuples (and furthermore the first element is an atom). You can break this down with a single generator in a for comprehension. Depending on your overall goal, you can process these easily, for example, to concat the field and the error message:

iex(1)> errors = [
...(1)>     email: {"can't be blank", [validation: :required]},
...(1)>     firstname: {"can't be blank", [validation: :required]},
...(1)>     lastname: {"can't be blank", [validation: :required]}
...(1)>   ]
[
  email: {"can't be blank", [validation: :required]},
  firstname: {"can't be blank", [validation: :required]},
  lastname: {"can't be blank", [validation: :required]}
]
iex(2)> for {field, {message, _kw_option}} <- errors, do: "#{field} #{message}"
["email can't be blank", "firstname can't be blank",
 "lastname can't be blank"]
3 Likes