【问题标题】:How to reduce a list to a single value如何将列表减少为单个值
【发布时间】:2019-10-01 17:21:41
【问题描述】:

我目前正在阅读 Programming Elixir 1.6 书,在第 88 页上创建一个接受列表和函数的函数(mapsum)是一个挑战。

这个mapsum函数应该以提供的函数为参数,通过递归遍历列表中的每个元素,在影响列表中的每个数字后,将所有结果相加。

例如:

[1, 2, 3], &(&1 * &1) - 传递给 mapsum 的参数。

预期结果:

[1, 4, 9] # Answer after each number in map has been effected by function.
[14] # Final answer after the values are added.

现在我可以得到[1, 4, 9] 的打印答案。
之后,我尝试添加将值相加的代码。

在尝试这样做时,我的 elixir 控制台会抛出一个错误,提示我将一个空列表作为第一个参数传递给函数 []。这是我现在正在做的以获得 [1, 4, 9] 答案:

def mapsum([], _fun), do: []

def mapsum([head | tail], func) do
    [func.(head) | mapsum(tail, func)]
end

所以你可以看到它接受一个列表和函数,就像挑战所说的那样。为了尝试添加以下值,我尝试了不起作用:

def mapsum([], _fun, val), do: []

def mapsum([head | tail], func, val \\ 0) do
    [func.(head) + val | mapsum(tail, func, val)]
end

在我看来,这完全有道理。我觉得好像我错过了 elixir 正在做的一个潜在的事情。

以下是我输入的内容所期望的结果:mapsum 传递了一个列表 [1, 2, 3] 和一个函数 &(&1 * &1),val 可以指定为 0,也可以单独保留,因为它有一个默认值值为 0。Mapsum 然后在传入列表的头部(第一个值)上运行 anon 函数。然后将返回的结果添加到 val,然后我们在尾部(或余数)上调用 mapsum,使用相同的函数和更新的 val。

这应该可以工作,直到一个空列表被传递并被第一个函数捕获。

我不确定我在这里缺少什么。我在编写 elixir 中似乎最简单的函数时遇到了很大的麻烦。对上述问题的答案并详细解释我所缺少的内容将非常有帮助,谢谢!

【问题讨论】:

    标签: elixir


    【解决方案1】:

    在我的Programming Elixir 1.6 副本中,练习在p. 77 上(不是第88 页)。

    Expected Outcome:
    
    `[1, 4, 9]` - Answer after each number in map has been effected by function.
    
    `[14]` - Final answer after the values are added.
    

    在练习描述之后,我的书中给出了这个例子:

    iex> MyList.mapsum [1, 2, 3], &(&1 * &1)
    14
    

    预期的结果是14,没有像[1, 4, 9]预期的中间结果。

    def mapsum([], _fun, val), do: []
    def mapsum([head | tail], func, val \\ 0) do
        [func.(head) + val | mapsum(tail, func, val)]
    end
    

    在我看来,这完全有道理。我觉得好像我错过了一个 elixir 正在做的底层事情......mapsum 然后运行匿名函数 在传入列表的头部(第一个值)。然后返回 结果被添加到 val,

    好吧,函数的返回值被添加到 val 变量的 value 中,但这个总和永远不会分配给 val 变量。这是iex中的一个例子:

    iex(5)> val = 0
    0
    
    iex(6)> val + 1
    1
    
    iex(7)> val
    0
    

    要向val 变量添加一些内容,您必须编写:

    iex(8)> val = val + 1
    1
    

    相反,你写的表达式是:

    [func.(head) + val | mapsum(tail, func, val)]
    

    mapsum 函数第一次执行时,val=0func.(head) 返回 1,因此 Elixir 将这些值替换到您的表达式中,为您提供:

    [1 + 0 | mapsum(tail, func, val)]
    

    或:

    [1 | mapsum(tail, func, val)]
    

    接下来,elixir 替换 tailfuncval 的值,得到:

    [1 | mapsum([2, 4], &(&1 * &1), 0)]  
    

    一种无需在代码中显式写入= 即可为变量分配新值的方法。假设你有这个定义:

    def repeat(_, 0), do: :ok
    
    def repeat(greeting, times) do
      IO.puts greeting
      repeat(greeting, times-1)
    end
    

    然后你像这样调用函数:

    repeat("hello", 4)
    

    repeat() 执行时,elixir 需要在函数体中计算以下表达式:

    repeat(greeting, times-1)
    

    repeat() 第一次执行时,greeting="hello"times=4,所以 elixir 首先将这些变量的值代入表达式,如下所示:

    repeat("hello", 4-1)  
    

    给你:

    repeat("hello", 3)
    

    接下来,该函数调用中的参数与函数参数变量匹配,如下所示:

            repeat("hello",    3 )     #<===function call
                      |        |
     greeting="hello" |        | times=3
                      V        V
        def repeat(greeting, times) do  #<====function definition
    

    换句话说,调用函数会导致参数隐式分配给函数参数变量。然后,在函数体内,您可以使用名称greetingtimes 来检索它们各自的值。

    但是,在你的表达中:

    [func.(head) + val | mapsum(tail, func, val)]
    

    部分:

    func.(head) + val
    

    不是函数调用的参数,因此总和不会分配给任何参数变量。

    剧透:我的解决方案如下
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _
    _

      def mapsum([head|tail], func) do
          func.(head) + mapsum(tail, func) 
      end
    
      def mapsum([], _func), do: 0
    

    该解决方案使用与 p. 73 上的示例相同的逻辑,即查找列表的长度:

    def len([head|tail]) do
      1 + len(tail)
    end
    

    ...终止的情况是:

    def len([]), do: 0
    

    与我上面的mapsum() 解决方案相反,我通常发现使用所谓的累加器 来设计递归解决方案更容易,这是一种基本的递归技巧。本书尚未在示例中使用累加器,但这里有一个使用累加器的简单 sum_list() 示例:

      def sum_list(list), do: sum_list(list, 0)
    
      def sum_list([], acc), do: acc
      def sum_list([head|tail], acc) do
        sum_list(tail, acc+head)
      end
    

    您将sum_list(list) 函数调用转换为带有两个参数的函数调用,在这一行中完成:

    def sum_list(list), do: sum_list(list, 0)
    

    第二个参数0 是所谓的累加器:它将累加您有兴趣在递归结束时返回的结果。注意,在elixir中,函数sum_list/1sum_list/2是完全不同的函数,彼此没有任何关系。如果它更容易理解,您可以为两个参数函数使用不同的名称,例如:

    def sum_list(list), do: my_helper(list, 0)
    

    如果list不为空,则两个参数的函数调用将匹配此函数子句:

      def sum_list([head|tail], acc) do
    

    0 将被分配给acc 参数变量。然后,您可以在 sum_list/2 定义的正文中为 acc 添加值:

    def sum_list([head|tail], acc) do
      new_acc = acc + head  #<==== HERE
      sum_list(tail, new_acc)  
    end
    

    ...并在递归函数调用中使用累加器的新值。注意最后的代码 sn-p 可以这样简化:

    def sum_list([head|tail], acc) do
      sum_list(tail, acc+head)  
    end
    

    然后,当列表尾部为空列表时,递归结束,返回累加器:

    def sum_list([], acc), do: acc
    

    我在编写看似最简单的内容时遇到了很大的麻烦 长生不老药中的功能。

    继续努力和尝试。当我开始使用 erlang 时,有时我会花一周时间尝试解决一个简单的递归问题。在您尝试进行此练习时,在我看来,您几乎发明了蓄能器概念,对您来说太好了。即使您没有找到解决方案,如果您挣扎了一会儿然后再查看解决方案,通常一个灯泡就会熄灭。使用递归会变得更容易。祝你好运!

    【讨论】:

    • 非常感谢。这真的很有帮助。我的第一语言是 JavaScript,所以我仍在努力摆脱面向对象语言的思维定势。我以前没有想过或听说过蓄能器,但这个想法完全有道理。感谢您的宝贵时间和帮助。
    • @JoeyBrown,不客气!如果你想要更多学习递归的资源,我可以推荐Programming Erlang
    【解决方案2】:
    def mapsum(list, fun, val \\ 0)
    def mapsum([], _fun, val), do: val
    def mapsum([head | tail], fun, val), do: mapsum(tail, fun, val + fun.(head))
    

    让我们逐行看一遍。

    我们的第一个定义只是声明第三个参数的默认值为0。仅此而已。

    我们的第二个定义说如果我们有一个空列表,它将返回当前的val。如果我们传入一个空列表(mapsum([], &amp;(&amp;1 * &amp;1))),这可能是0,或者如果传入一个非空列表(mapsum([1,2,3], &amp;(&amp;1 * &amp;1))),它可能是累加值。

    我们的第三个定义是主要逻辑的实际所在。它使用模式匹配来分离列表的headtail。然后它使用tail 作为新的第一个参数递归调用我们的函数,保持fun 不变,并更改val 以便它采用当前val 并将fun 的结果应用到head

    让我们看看这实际上是如何扩展的。

    mapsum([1,2,3], &(&1 * &1))
    
    mapsum([2,3], &(&1 * &1), 0 + fun.(1)
    mapsum([2,3], &(&1 * &1), 1)
    
    mapsum([3], &(&1 * &1), 1 + fun.(2)
    mapsum([3], &(&1 * &1), 5)
    
    mapsum([], &(&1 * &1), 5 + fun.(3)
    mapsum([], &(&1 * &1), 14)
    
    14
    

    这与您的方法不同,因为我在这里进行总结,您的解决方案在其中完成了 map 部分(它创建了一个新列表,其中 fun 应用于每个元素),但您无法应用新列表中的实际 sum 部分。

    【讨论】:

      【解决方案3】:

      我想挑战是不使用 Enum 模块来实现它。我仍然会先使用 Enum 模块来实现它,然后重新实现 Enum 中使用的函数。

      defmodule MyList 
        do def mapsum(list, f) do 
          Enum.map(list, f) |> Enum.sum 
        end 
      end
      

      下一步是重新实现 map 和 sum

      defmodule MyList do
        def mapsum(list, f) do
          map(list, f) |> sum
        end
      
        defp sum([head | tail]) do
          head + sum(tail)
        end
        defp sum([]) do
          0
        end
      
        defp map([head | tail], f) do
          [f.(head)|map(tail, f)]
        end
        defp map([], _f) do
          []
        end
      
      end
      

      sum 和 map 都可以使用 reduce 来实现,这可能是一个后续练习 :)

      【讨论】:

        【解决方案4】:

        这是在做什么:

        def mapsum([head | tail], func, val \\ 0) do
            [func.(head) + val | mapsum(tail, func, val)]
        end
        

        在每个元素上运行func,并向其中添加0(或val) - 但val 保持不变并且永远不会更新,并且该函数仍然返回一个列表。

        我建议您将val 重命名为acc,因为它应该保存累积值。然后你可以这样写:

        def mapsum([], _fun, acc), do: acc
        
        def mapsum([head | tail], func, acc \\ 0) do
          mapsum(tail, func, acc + func.(head))
        end
        

        这做了两件事:

        1. 当列表为空时,打印accumulated 值。
        2. 当列表不为空时,用尾部递归,将头部加到累加值中。

        【讨论】:

          猜你喜欢
          • 2015-01-03
          • 1970-01-01
          • 1970-01-01
          • 2016-11-24
          • 2011-06-09
          • 2017-09-11
          • 2011-07-31
          • 2019-04-29
          • 2019-11-18
          相关资源
          最近更新 更多