Is it possible to partially pattern-match Lists as we do with Maps?

Hello everybody,

As it’s possible to partially pattern match with maps like so:

iex> %{b: 2} = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}

Is it possible to do the same with Lists, e.g. like that:

iex> [2] = [1, 2, 3]
# MatchError

I guess that the inherent nature of linked lists (which make it easy to pattern-match heads/tails) might complicate things. Also I got the fact that in this case it might not make sense to pattern-match for variable binding . But it might be useful to pattern-match in this case for conditions/tests.

One can simply use Enum.member? but I don’t see an easy way to avoid conditions like we do when using pattern-matching in general or in functions argument.

Thank you very much for any details…

Hi @Sanjibukai you can match on the head and tail of a list, but there is no way to do a match which asks “Is this value anywhere in the list”.

[head | tail] = [1,2,3]
head #=> 1
tail #=> [2,3]
2 Likes

Hello,

Yes I know about head/tail pattern-matching for Lists but I was curious to know if more elaborated options exists… Maybe with more advanced Elixir Data Structure besides Maps?

Just noticed the tag besides your username… I bought your book :wink:. A long time ago tbh… But it’s on the queue… I’m currently learning Elixir/Phoenix basics from other PragProg books I have…

As @benwilson512 said, you cannot pattern match in the middle of a list. If you are looking for a convenient and readable way to check for inclusion, you can do this though:

2 in [1, 2, 3]

You can also use it in guards:

case 2 do
  x when x in [1, 2, 3] -> :yes
  _ -> :no
end
2 Likes

I forgot that notation… Indeed it’s great!

Then I guess that it might work in a pattern-matching when matching with true, like so:

def some_func(true) do
  # value present
end

def some_func(false) do
#  value absent
end

some_func(val in list)

Or even easier, as you can use it in guards:

def some_func(x) when x in [1, 2, 3] do
  :present
end

def some_func(_) do
  :not_present
end
4 Likes

Is it possible to use that bit (conditional of the case) in a function argument as pattern-matching purpose?

EDIT: Thanks I saw your answer above!

Yes, like I posted before (we’re replying to each other fast so some posts are probably noticed only after pressing “reply” :slight_smile: )

EDIT: indeed it happened again :smile:

1 Like

Yeah I noticed it, and edited!
in fact you just gave me what I was looking for (namely guard clause) but I might stated the problem wider to learn a little more :wink:

1 Like

It’s interesting to notice how the guard is implemented. It is documented here and basically translates this:

when x in [1, 2, 3]

Into this:

when x === 1 or x === 2 or x === 3

The guard also works for ranges, with a different implementation, as the documentation shows.

2 Likes

Yeah, this different implemantation introduces slightly “bugs” if not understood correctly:

def f(x) when x in [1,2,3], do: "123"
def f(x) when x in 4..6, do: "456"
def f(_), do: "nope"

IO.puts f(2) #=> 123
IO.puts f(2.0) #=> nope
IO.puts f(2.5) #=> nope
IO.puts f(5) #=> 456
IO.puts f(5.0) #=> 456
IO.puts f(5.5) #=> 456
4 Likes

According to the doc linked by @lucaong it should still work in these examples since:

when x in 1..3
translates to:
when is_integer(x) and x >= 1 and x <= 3
Note that only integers can be considered inside a range by in .

Also, I just tried and indeed I got:

IO.puts f(5) #=> 456
IO.puts f(5.0) #=> nope
IO.puts f(5.5) #=> nope

Edit: I think that this bit is_integer(x) and might be an addition brought by an update later on…
Also it’s still interesting to know and behaves carefully with edge cases.

Important thing to notice in order to understand the behavior of in with ranges, is that a range is an Enumerable of integers. Therefore, (0..10) includes 5 but not 5.5, nor 5.0 (see the is_integer(x) in the guard expansion).

That is consistent with how Enum.member? works on ranges:

Enum.member?(0..10, 5) #=> true
Enum.member?(0..10, 5.5) #=> false
Enum.member?(0..10, 5.0) #=> false

In other words, it could be considered surprising, but it is not a bug, it is intended behavior.

2 Likes

Oh, nice, Then either things have changed with newer versions of elixir, or something else bit me years ago…

2 Likes

In fact I just checked and indeed…
It happened 3 years ago

1 Like

Thats just documentation… I went back in that file another 2 years, and the is_integer/1 check for left has been there in every commit…

1 Like

Indeed… My bad…
It was 5 years ago

EDIT: It’s still not that… I went to fast with the commit messages…

This is quite surprising but makes me feel really good about myself because I always assert on the type first and then on the range of acceptable values. :024:

EDIT: Oops, I didn’t read all comments before commenting but the remark still stands.