如何在Elixir中按关键字拆分列表

时间:2016-08-22 20:13:02

标签: elixir

假设我有一个单词列表,其中一个关键字,在本例中为“停止”,划分完整的句子:

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

我想转变成:

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

我知道我可以用String.split的字符串做到这一点,但理想情况下我想学习如何用基本的功能结构解决上述问题,比如[head | tail]等的递归,但我无法想象从哪里开始如何累积中间列表。

4 个答案:

答案 0 :(得分:7)

您可以使用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.292903s | 0.316024s | 0.292106s |的 chunk_by

     

0.168113s | 0.152456s | 0.151854s | Main.main (@Dogbert's answer)

     

0.167387s | 0.148059s | 0.143763s | chunk_on (@Martin Svalin's answer)

     

0.177080s | 0.180632s | 0.185636s | splitter (@stephen_m's answer)

答案 1 :(得分:3)

这是一个使用模式匹配的简单尾递归实现:

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"]]

答案 2 :(得分:3)

几乎 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。由于我们在前面构建了累加器,将我们找到的第一个元素推向后面,因此在减少它之前反转列表是有意义的。否则,我们最终会得到一个反向列表的反向列表。

我们可能会获得空列表,例如示例列表中的最终: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所做的那样,正如我们在完成后拒绝空列表一样。同样的理由适用。

我们使用模式匹配来识别停用词。停用词标记了新列表的开头,因此我们添加一个新的空列表并丢弃停用词。

常规单词只是放在第一个列表的前面,在我们的列表列表中。所有这些条和括号的语法都有点笨拙。

答案 3 :(得分:2)

就个人而言,我最喜欢AbM's的答案,而且由于易于阅读,我更喜欢这个答案。

那就是说,我没有兴趣看看是否可以在没有最终Enum.reject功能的情况下完成。

  def splitter(list) do
    res =
    List.foldl(list, [], fn(word, acc)->
      case {word, acc} do
        {"stop", []} ->
          []
        {word, []} ->
          [[word]]
        {"stop", [[], acc]} ->
            [h | t] = acc
            [Enum.reverse(h) | t]
        {"stop", acc} ->
          [h | t] = acc
          [[] | [Enum.reverse(h) | t]]
        {word, [[] | acc]} ->
          [[word] | acc]
        {word, acc} ->
          [h | t] = acc
          new_h = [word | h]
          if t == [], do: [new_h], else: [new_h | t]
     end
    end)
    res = if List.first(res) == [], do: ([h | t] = res; t), else: (res)
    Enum.reverse(res)
  end

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

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

查看代码有点令人头疼,我可能不会因为这个原因而使用它,但我认为它运行得更快。