Recursion 在Elixir中使用模式匹配和递归拆分列表

Recursion 在Elixir中使用模式匹配和递归拆分列表,recursion,pattern-matching,elixir,Recursion,Pattern Matching,Elixir,我不熟悉Elixir,也不熟悉编程,尤其是函数式编程(在Ruby和RoR方面的经验不足1年)。目前我正在读戴夫·托马斯的《编程长生不老药》。我完全被列表和递归主题中的一个问题所困扰 Dave要求“不使用库函数或列表理解实现以下枚举函数:…拆分…” 原来的功能是 我用相当长的时间解决问题,可能不是太理想(在我看来,这部分违反了Dave的限制): 在我看来,这段代码相当优雅,但我无法理解它是如何工作的。 我的意思是,我试图一步一步地理解发生了什么,但我失败了 我可以想象在我的filter1递归函数中

我不熟悉Elixir,也不熟悉编程,尤其是函数式编程(在Ruby和RoR方面的经验不足1年)。目前我正在读戴夫·托马斯的《编程长生不老药》。我完全被列表和递归主题中的一个问题所困扰

Dave要求“不使用库函数或列表理解实现以下枚举函数:…拆分…”

原来的功能是

我用相当长的时间解决问题,可能不是太理想(在我看来,这部分违反了Dave的限制):

在我看来,这段代码相当优雅,但我无法理解它是如何工作的。 我的意思是,我试图一步一步地理解发生了什么,但我失败了

我可以想象在我的
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