假设我有一个单词列表,其中一个关键字,在本例中为“停止”,划分完整的句子:
["Hello", "from", "Paris", "stop", "Weather", "is", "sunny", "stop", "Missing", "you", "stop"]
我想转变成:
[["Hello", "from", "Paris"], ["Weather", "is", "sunny"], ["Missing", "you"]]
我知道我可以用String.split的字符串做到这一点,但理想情况下我想学习如何用基本的功能结构解决上述问题,比如[head | tail]等的递归,但我无法想象从哪里开始如何累积中间列表。
答案 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"]]
查看代码有点令人头疼,我可能不会因为这个原因而使用它,但我认为它运行得更快。