5
votes

I have a list of structs of an arbitrary size.

Let's call it l.

l = [%X{a:1}, %X{a:3}, %X{a:9}, %X{a:11}]

The size of l keeps changing. What I'd like to know is how do I pattern match against l to ensure it always is made up of structs of %X{}. I want the pattern matching to fail if the list contains something else. For example:

l = [%X{a:1}, %X{a:3}, %Y{a:9}, %Z{a:11}]

Things I've tried

i = %X{}
j = %Y{}

[%X{}|_] = [i,i,i]

But that matches only the first element.

[%X{}|_] = [i,j,j]

Should fail for my use case, but it doesn't. Maybe if there's an operator or something like this, that will match a list of specific type, that's exactly what I'm looking for:

[%X{}+] = [i,i,i] # Doesn't exist, just an example

Some background

I'm on phoenix and I have a model post with has_many relationship with images. A given user could upload multiple images and I'd like to pattern match to make sure I'm working with the right struct (%Plug.Upload{}) in this case.

Any help is much appreciated. Thanks :)

5

5 Answers

8
votes

You cannot pattern match on every element of a list (without recursion). In this case I'd use Enum.all?/2 and the match?/2 macro:

if Enum.all?(list, &match?(%X{}, &1)) do
  ...
end
3
votes

While the answer by @Dogbert is perfectly valid, I personally find the explicit clauses to be more succinct:

all_are_x =
  Enum.all?(list, fn
    %X{} -> true
    _ -> false
  end)
1
votes

AFAIK, what you are looking for doesn't exist: you cannot pattern match on every element of a list.

You can use Enum.map/2, to crash at the first non %X{} element:

Enum.map(l, &(%X{}=&1))

To pattern match, I used: %X{} = something, while Dogbert used: match?(%X{}, &1)

The difference is that first one fails if it doesn't match, while the second one returns false. If you want to stick to "let it crash" of elixir, you might be interested in the first one, while in most of the case you will prefer to use the second one, like this for example:

k == Enum.reject(l, &match?(%X{}=&1))
k == [] || IO.inspect(k)

Enum.reject?/2 used with match?/2 will not crash and return a list of all the element that are not an X structure.

0
votes

How I ended up solving this:

As described in my question, I was trying to match each instance of uploaded file to ensure it's a %Plug.Upload{} struct. I should have mentioned I am on Phoenix. The good thing on Phoenix is you can add this sort of validation directly into your models - that's what changesets and their validations are for! And they will be validated automatically for each instance of the model being saved to the database.

So, I ended up adding a custom validation in my image.ex model.

So, my changeset ended up looking like:

  @doc false
  def changeset(image, attrs) do
    image
    |> cast(attrs, [:title, :description, :alt, :src, :sequence, :file])
    |> ensure_valid_upload()
    |> validate_required([:file])
  end

Having said that, I did also end up using the accepted answer in other areas of my code which weren't directly related to my models.

Hope this helps someone in the future.

0
votes

I do this way when I want only atoms for example. I is rather blatant but I like keep the code explicit at usage:

  def only_atoms(list) when is_list list do
    Enum.map(list, &only_atom/1)
  end

  def only_atom(x) when is_atom x do
    x
  end

I guess it is rather like the crash version of @Aleksei Matiushkin. Would be nice to have something more general available as a guard clause.