Как разделить список по ключевым словам в Elixir

Допустим у меня есть список слов, слово, в данном случае "стоп", разграничивает полными предложениями:

["Hello", "from", "Paris", "stop", "Weather", "is", "sunny", "stop", "Missing", "you", "stop"]

который я хочу превратить в:

[["Hello", "from", "Paris"], ["Weather", "is", "sunny"], ["Missing", "you"]]

Я знаю, что могу сделать это со строками строки.split, но в идеале я хотел бы узнать, как решить вышеуказанную проблему с фундаментальными функциональными конструкциями, такими как рекурсия на [head|tail] и т. д., Но я не могу понять, с чего начать, как накапливать промежуточные списки.

4 ответов


вот простая хвостовая рекурсивная реализация с использованием сопоставления шаблонов:

defmodule Main do
  def split_on(list, on) do
    list
    |> Enum.reverse
    |> do_split_on(on, [[]])
    |> Enum.reject(fn list -> list == [] end)
  end

  def do_split_on([], _, acc), do: acc
  def do_split_on([h | t], h, acc), do: do_split_on(t, h, [[] | acc])
  def do_split_on([h | t], on, [h2 | t2]), do: do_split_on(t, on, [[h | h2] | t2])

  def main do
    ["Hello", "from", "Paris", "stop", "Weather", "is", "sunny", "stop", "Missing", "you", "stop"]
    |> split_on("stop")
    |> IO.inspect
  end
end

Main.main

выход:

[["Hello", "from", "Paris"], ["Weather", "is", "sunny"], ["Missing", "you"]]

можно использовать chunk_by/2:

["Hello", "from", "Paris", "stop", "Weather", "is", "sunny", "stop", "Missing", "you", "stop"]    
|> Enum.chunk_by(fn(x) -> x != "stop" end) 
|> Enum.reject(fn(x) -> x == ["stop"] end)

производительность

из любопытства я хотел бы сравнить производительность реализаций, данных этому вопросу. Эталон был для 100 000 вызовов каждой реализации, и я запустил его 3 раза. Вот результаты, если кто-то заинтересован:

0.292903 s | 0.316024 s | 0.292106 s/chunk_by

0.168113 s / 0.152456 s / 0.151854 s | Main.основной (ответ@Dogbert х)

0.167387 s | 0.148059 s | 0.143763 s/chunk_on (ответ@Мартин Svalin х)

0.177080 s | 0.180632 s | 0.185636 s/splitter (ответ@stephen_m)


это почти что Enum.chunk_by/2 делает.

def chunk_by (перечисляемый, весело)

разбивает перечисляемое на каждый элемент, для которого fun возвращает новое значение.

но chunk_by не выбрасывать какие-либо элементы, поэтому мы можем объединить его с Enum.filter/2.

list = [1, 2, 3, :stop, 4, 5, 6, :stop, 7, 8, :stop] # analogous to your list

list
|> Enum.chunk_by(&(&1 == :stop))
   # at this point, you have [[1,2,3], [:stop], [4,5,6], [:stop], [7,8], [:stop]]
|> Enum.reject(&(&1 == [:stop]))
   # here you are: [[1,2,3], [4,5,6], [7,8]]

второй подход будет использовать Enum.reduce/3. Поскольку мы строим аккумулятор спереди, толкая первые элементы, которые мы находим в обратном направлении имеет смысл перевернуть список, прежде чем мы его уменьшим. В противном случае мы получим перевернутый список перевернутых списков.

мы потенциально получим пустые списки, такие как final :stop в нашем примере список. Итак, мы снова фильтруем список в конце.

list
|> Enum.reverse
|> Enum.reduce([[]], fn         # note: the accumulator is a nested empty list
  :stop, acc -> [[] | acc]      # element is the stop word, start a new list
  el, [h | t] -> [[el | h] | t] # remember, h is a list, t is list of lists
end)
|> Enum.reject(&Enum.empty?/1)

наконец, давайте пройдемся по списку сами и построим аккумулятор. Если это напоминает вам о reduce версия, это не совпадение.

defmodule Stopword do
  def chunk_on(list, stop \ :stop) do
    list
    |> Enum.reverse
    |> chunk_on(stop, [[]])
  end

  defp chunk_on([], _, acc) do
    Enum.reject(acc, &Enum.empty?/1)
  end
  defp chunk_on([stop | t], stop, acc) do
    chunk_on(t, stop, [[] | acc])
  end
  defp chunk_on([el | t], stop, [head_list | tail_lists]) do
    chunk_on(t, stop, [[el | head_list] | tail_lists])
  end
end

мы используем общепринятые шаблон публичной функции, которая не требует от пользователей беспокоиться о аккумуляторе, и передача входов в частную функцию arity+1 с аккумулятором. Поскольку мы создаем список списков, полезно начать с аккумулятора с пустым списком внутри него. Таким образом, нам не нужно особый случай, когда аккумулятор пуст.

мы перевернем список, прежде чем идти по нему, как мы сделали для reduce так же, как мы отвергаем пустые списки после того, как мы закончим. Тем же причинам применять.

мы используем сопоставление шаблонов для идентификации стоп-слова. Стоп-слово знаменует начало нового списка, поэтому мы добавляем новый пустой список и выбросить стоп-слова.

обычное слово просто помещается в начале первого списка, в нашем списке списков. Синтаксис немного громоздкий со всеми этими барами и скобками.


лично мне нравится AbM's ответ лучшим, и я предпочитаю его над этим ответом Из-за удобства чтения.

тем не менее, я решил из интереса посмотреть, можно ли это сделать без окончательного