1
votes

Is there a way to pattern match something like [_head | middle | _last] in Elixir? That is, I want to match on a list that has at least three things in it (but often more), but only match on everything but the head and last element.

Example:

    def match_middle([_head | middle | _last]) do
      IO.inspect middle
    end

    match_middle [1, 2, 3, 4, 5]
    >>> [2, 3, 4]
3
Probably there is no wayNoDisplayName
Are you sure you want to pattern match and not just extract the list without the first and last elements?GavinBrelstaff
@GavinBrelstaff I was wondering specifically about pattern matching, but if you have a succinct way of doing it, I'd like to see it. I am currently using the first thing that came to mind. I don't see a way to format code in comments.... Here it is with semi-colons to guide the way. [_ | rest] = items; [_last | rest_backwards] = Enum.reverse rest; middle = Enum.reverse(rest_backwards)MaxStrange

3 Answers

5
votes

Elixir lists are really cons lists that go way back into the early days of Lisp.

iex(1)> foo = [1 ,2 , 3]
[1, 2, 3]
iex(2)> [ head | [ middle | tail ]] = foo
[1, 2, 3]
iex(3)> middle
2

So while we write it as [1,2,3] it is really [1, [2,[3,[]]]]

However, that really doesn't cover what you are asking. Something like this maybe

def match_middle([_head | [ second | tail]]) when tail != [] do
  flip_tail = Enum.reverse(tail)
  [end | rest] = flip_tail
  [second | Enum.reverse(rest) ]
end
2
votes

Propably I would solve that without pattern matching, because your case is quite specific - you assume that middle is everything except first and last elements, but split operator works in different manner - it splits list for first element and everything else as tail.

In your case I would create private function for matching the middle:

defp get_middle_from_list(list) do
  list 
  |> Stream.drop(1)
  |> Stream.drop(-1)
  |> Enum.to_list()
end

and use it

get_middle_from_list [1, 2, 3, 4, 5]
> [2, 3, 4]

Of course it will return [] if list contain less then 3 elements.

1
votes

As you might see, List.pop_at/3 is itself implemented using recursion, :lists.reverse, and head access. That said, there is no way to directly access the last element in the list.

On the other hand, if you are concern, that there can’t be more than, say, N elements in the list, the direct pattern match is still achievable:

defmodule MatchInTheMiddle do
  @n 10
  Enum.each(0..@n, fn i ->
    @match_clause Enum.map(0..i, fn j -> {:"p#{j}", [], Elixir} end)
    def match_middle(unquote(
      [{:_head, [], Elixir}] ++ @match_clause ++ [{:_last, [], Elixir}])
    ), do: IO.inspect unquote(@match_clause)
  end)
end

And, yes, the graceful fallback for short and long lists:

def match_middle([]), do: []
def match_middle([_]), do: []
def match_middle([_head | tail]) do
  with [_tail | middle] <- :lists.reverse(tail), do: middle
end

MatchInTheMiddle.match_middle([1,2,3,4])
#⇒ [2,3]

This is not as elegant as you probably expected it to be, but in some cases it might be helpful, since these clauses are compiled into pattern matches and on huge amounts of data this is rather more efficient that Enum operations.