Как разделить список по ключевым словам в 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
ответ лучшим, и я предпочитаю его над этим ответом Из-за удобства чтения.
тем не менее, я решил из интереса посмотреть, можно ли это сделать без окончательного