Recursion 在Elixir中使用模式匹配和递归拆分列表
我不熟悉Elixir,也不熟悉编程,尤其是函数式编程(在Ruby和RoR方面的经验不足1年)。目前我正在读戴夫·托马斯的《编程长生不老药》。我完全被列表和递归主题中的一个问题所困扰 Dave要求“不使用库函数或列表理解实现以下枚举函数:…拆分…” 原来的功能是 我用相当长的时间解决问题,可能不是太理想(在我看来,这部分违反了Dave的限制): 在我看来,这段代码相当优雅,但我无法理解它是如何工作的。 我的意思是,我试图一步一步地理解发生了什么,但我失败了 我可以想象在我的Recursion 在Elixir中使用模式匹配和递归拆分列表,recursion,pattern-matching,elixir,Recursion,Pattern Matching,Elixir,我不熟悉Elixir,也不熟悉编程,尤其是函数式编程(在Ruby和RoR方面的经验不足1年)。目前我正在读戴夫·托马斯的《编程长生不老药》。我完全被列表和递归主题中的一个问题所困扰 Dave要求“不使用库函数或列表理解实现以下枚举函数:…拆分…” 原来的功能是 我用相当长的时间解决问题,可能不是太理想(在我看来,这部分违反了Dave的限制): 在我看来,这段代码相当优雅,但我无法理解它是如何工作的。 我的意思是,我试图一步一步地理解发生了什么,但我失败了 我可以想象在我的filter1递归函数中
filter1
递归函数中发生了什么。列表是这样形成的:[head_1 |……head_n | filter1(tail_n,count-n)]
但是我不明白为什么{left,right}
元组匹配函数的递归调用。什么应该与左边的匹配,什么应该与右边的匹配?这个递归是如何工作的
(第二行(函数)的含义我也不清楚,但我认为这与第一个问题密切相关。)
UPD:
多亏了@Josh Petitt、@tkowal和@CodyPoll,我想我对这个案子的理解有所进步
现在,我在考虑以这种“金字塔方式”讨论的递归匹配模式:
- 第一步(第1行):调用函数
- 第二步(第2、3行):将
{left,right}
元组与递归函数调用匹配,并返回{[1 | left],right}
元组
- 第三步(第4、5行):将
{left,right}
元组匹配到下一个递归调用,并返回{[1 |[2 | left]],right}
元组
- 第四步(第6行):由于
split([3],0)
与第二个子句匹配,此时我们得到{left,right}={[],[3]}
,并且我们不能相应地用[]和[3]替换第5行中的left
和right
变量
- 第五步(第7行):“管道”完成它们的工作并返回列表以最终匹配
left
变量
我仍然不明白的是人们是如何找到这种解决方案的?(可能有模式匹配和递归的经验。)
还有一件事困扰着我。以第3行为例,它是一个包含两个变量的“返回”。但实际上没有任何值与该变量匹配。根据我的方案,这些变量只与第7行中的值匹配
长生不老药是怎么处理的?
它是某种隐式的nil
匹配吗?
或者我的流程错误,直到最后一步才有实际的返回?#第一个元素是head,尾部是列表的其余部分
# the first element is head, the tail is the rest of the list
# count must be greater than 0 to match
def split([head | tail], count) when count > 0 do
# recursively call passing in tail and decrementing the count
# it will match a two element tuple
{left, right} = split(tail, count-1)
# return a two element tuple containing
# the head, concatenated with the left element
# and the right (i.e. the rest of the list)
{[head | left], right}
end
# this is for when count is <= 0
# return a two element tuple with an empty array the rest of the list
# do not recurse
def split(list, _count), do: {[], list}
#计数必须大于0才能匹配
当计数>0时,def拆分([头|尾],计数)
#递归调用传入尾部并递减计数
#它将匹配一个两元素元组
{left,right}=split(tail,count-1)
#返回一个包含
#头部,与左侧元素连接
#和右侧(即列表的其余部分)
{[头|左,右}
结束
#这适用于计数时,代码比较复杂,因为它不是尾部递归,所以它不是循环,并且它记住O(n)个调用
让我们尝试分析一个简单的示例,其中缩进表示递归级别:
split([1,2,3], 2) ->
#head = 1, tail = [2,3], count = 2
{left, right} = split([2,3], 1) -> #this is the recursive call
#head = 2, tail = [3], count = 1
{left, right} = split([3], 0) #this call returns immediately, because it matches second clause
{left, right} = {[], [3]} #in this call
#what we have now is second list in place, we need to reassemble the first one from what we remember in recursive calls
#head still equals 2, left = [], right = [3]
{[head | left], right} = {[2], [3]} #this is what we return to higher call
#head = 1, left = [2], right = [3]
{[head | left], right} = {[1,2], [3]}
因此,模式是分解列表并在递归中记住其元素,然后重新组装它。这种模式的最简单情况是:
def identity([]) -> []
def identity([head | tail]) do
# spot 1
new_tail = identity(tail)
# spot 2
[head | tail]
end
此函数对原始列表不做任何更改。它只遍历所有元素。要了解模式,请猜一下当您放置IO时会发生什么。将头部放置在点1和点2
然后尝试修改它,只遍历元素的计数,然后您将看到您离拆分实现有多近。递归有时很难理解,只看代码。精神上跟踪放在堆栈上的内容、检索内容以及检索时间可以很快耗尽我们的工作记忆。在递归树的层次结构中画出每一段的路径是很有用的,这就是我试图回答你的问题所做的
为了理解本例中的工作原理,首先我们必须认识到在第1条中存在两个不同的阶段,第一阶段是递归之前执行的代码,第二阶段是递归之后执行的代码
(为了更好地解释流程,我在原始代码中添加了一些变量)
现在,在继续阅读之前,请查看代码并尝试回答以下问题:
- 第一个块迭代多少次后,
结果
变量将第一次绑定
- 在子句1中,递归
拆分(tail,count-1)
将被调用多少次
- 第2条
拆分(列表,计数)
将被调用多少次
- 第2条的作用是什么
现在比较一下你的答案,看看这个显示每一段及其层次结构的模式:
(例如,我们将列表[1,2,3,4,5]
拆分到第三个元素之后,以获得元组{[1,2,3],[4,5]}
)
同时,迭代继续的开始标记为
< I'm BACK to the SECOND STAGE of ITERATION n
然后将头
推到堆栈上,将尾
和更新计数器
传递给递归:
result = split(tail, count - 1)
在count
迭代之后,所有左分割的元素都在堆栈上,所有右分割的元素都打包在tail
中。条例草案第2条现予通过
在子句2调用之后,递归继续
def identity([]) -> []
def identity([head | tail]) do
# spot 1
new_tail = identity(tail)
# spot 2
[head | tail]
end
# Clause 1
def split(in_list, count) when count > 0 do
# FIRST STAGE
[head | tail] = in_list
# RECURSION
result = split(tail, count - 1)
# SECOND STAGE
{left, right} = result
return = {[head | left], right}
end
#Clause 2
def split(list, _count), do: return = {[], list}
split([1,2,3,4,5], 3)
> FIRST STAGE of CLAUSE 1 / ITERATION 1 called as: split( [1, 2, 3, 4, 5], 3 ):
Got 'head'=1, 'tail'=[2, 3, 4, 5], 'count'=3
now I'm going to iterate passing the tail [2, 3, 4, 5],
Clause 1 will match as the counter is still > 0
> FIRST STAGE of CLAUSE 1 / ITERATION 2 called as: split( [2, 3, 4, 5], 2 ):
Got 'head'=2, 'tail'=[3, 4, 5], 'count'=2
now I'm going to iterate passing the tail [3, 4, 5],
Clause 1 will match as the counter is still > 0
> FIRST STAGE of CLAUSE 1 / ITERATION 3 called as: split( [3, 4, 5], 1 ):
Got 'head'=3, 'tail'=[4, 5], 'count'=1
Now the counter is 0 so I've reached the split point,
and the Clause 2 instead of Clause 1 will match at the next iteration
> Greetings from CLAUSE 2 :-), got [4, 5], returning {[], [4, 5]}
< Im BACK to the SECOND STAGE of ITERATION 3
got result from CLAUSE 2: {[], [4, 5]}
{left, right} = {[], [4, 5]}
Now I'm build the return value as {[head | left], right},
prepending 'head' (now is 3) to the previous value
of 'left' (now is []) at each iteration,
'right' instead is always [4, 5].
So I'm returning {[3], [4, 5]} to iteration 2
< Im BACK to the SECOND STAGE of ITERATION 2
got result from previous Clause 1 / Iteration 3, : {[3], [4, 5]}
{left, right} = {[3], [4, 5]}
Now I'm build the return value as {[head | left], right},
prepending 'head' (now is 2) to the previous value
of 'left' (now is [3]) at each iteration,
'right' instead is always [4, 5].
So I'm returning {[2, 3], [4, 5]} to iteration 1
< Im BACK to the SECOND STAGE of ITERATION 1
got result from previous Clause 1 / Iteration 2, : {[2, 3], [4, 5]}
{left, right} = {[2, 3], [4, 5]}
Now I'm build the return value as {[head | left], right},
prepending 'head' (now is 1) to the previous value
of 'left' (now is [2, 3]) at each iteration,
'right' instead is always [4, 5].
And my final return is at least: {[1, 2, 3], [4, 5]}
{[1, 2, 3], [4, 5]}
> FIRST STAGE of CLAUSE 1 / ITERATION n called as: ...
< I'm BACK to the SECOND STAGE of ITERATION n
# FIRST STAGE
[head | tail] = in_list
result = split(tail, count - 1)
return = {[head | left], right}
def split(in_list, count), do: split(in_list, count, 1)
# Clause 1
def split(in_list=[head | tail], count, iteration) when count > 0 do
offset = String.duplicate " ", 5 * (iteration - 1)
IO.puts offset <> "> FIRST STAGE of CLAUSE 1 / ITERATION #{inspect iteration} called as: split( #{inspect in_list}, #{inspect(count)} ):"
IO.puts offset <> " Got 'head'=#{inspect head}, 'tail'=#{inspect tail}, 'count'=#{inspect count}"
if (count - 1) > 0 do
IO.puts offset <> " now I'm going to iterate passing the tail #{inspect(tail)},"
IO.puts offset <> " Clause 1 will match as the counter is still > 0"
else
IO.puts offset <> " Now the counter is 0 so I've reached the split point,"
IO.puts offset <> " and the Clause 2 instead of Clause 1 will match at the next iteration"
end
result = split(tail, count-1, iteration + 1)
IO.puts offset <> "< Im BACK to the SECOND STAGE of ITERATION #{inspect(iteration)}"
if (count - 1) == 0 do
IO.puts offset <> " got result from CLAUSE 2: #{inspect result}"
else
IO.puts offset <> " got result from previous Clause 1 / Iteration #{iteration + 1}, : #{inspect result}"
end
IO.puts offset <> " {left, right} = #{inspect result}"
{left, right} = result
IO.puts offset <> " Now I'm build the return value as {[head | left], right},"
IO.puts offset <> " prepending 'head' (now is #{inspect head}) to the previous value"
IO.puts offset <> " of 'left' (now is #{inspect left}) at each iteration,"
IO.puts offset <> " 'right' instead is always #{inspect right}."
return = {[head | left], right}
if (iteration > 1) do
IO.puts offset <> " So I'm returning #{inspect return} to iteration #{inspect(iteration - 1)}"
else
IO.puts offset <> " And my final return is at least: #{inspect return} "
end
return
end
# Clause 2
def split(list, _count, _iteration) do
IO.puts ""
IO.puts "> Greetings from CLAUSE 2 :-), got #{inspect(list)}, returning #{inspect({[], list})}"
IO.puts ""
{[], list}
end