枚举减少递归:第一次迭代后确定

时间:2019-03-26 06:30:33

标签: elixir

我要传递一个由整数组成的列表,作为Enum.reduce中的字符串,并在列表的每个值上运行一个函数。但是问题是在第二次迭代中它返回:ok,由于哪个函数给了错误,我在代码中使用了多个IO.puts只是为了跟踪错误。

下面是我的函数to_decimal,当我们将“ 11”作为参数传递时,它应返回此二进制值的十进制表示形式。但是它会在第二次迭代中停止,我将在下面写入输出-

def to_decimal(string) do
    list = String.graphemes(string)
    len = length(list)
    Enum.reduce(list,0, fn x,acc ->
            temp=String.to_integer(x)
            power = round(:math.pow(2,len-1))
            IO.puts(power)
            acc = acc + temp*power
            IO.puts(acc)
            len=len-1
            IO.puts(len)
            end)
end

iex(1)> Binary.to_decimal("11")
2
2
1
2
** (ArithmeticError) bad argument in arithmetic expression: :ok + 2
    :erlang.+(:ok, 2)
    binary.exs:16: anonymous fn/3 in Binary.to_decimal/1
    (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3

3 个答案:

答案 0 :(得分:3)

您应该在每次迭代中返回新的acc。当前,“回调”函数中的最后一个表达式(即reduce(enumerable, acc, fun)中的fun)是IO.puts,它返回:ok。您应该在每次迭代中构建新的acc并返回它,该值将在下一次迭代中使用。

请注意,在Elixir中,数据结构是不可变的。还要注意,您不会更改存在于上限范围内的len。基本上您在做的是throw_away = len - 1

如果想进一步了解变量范围,请查看this answer

答案 1 :(得分:3)

对于真实世界的代码,您应该使用预发布的解决方案:

String.to_integer(string, 2)

话虽如此,让我们将此作为练习来更多地了解Elixir。 Elixir的一个常见陷阱是对可变范围的误解。 Elixir中的变量是不可变的,并且范围始终是局部的,但是它们可以反弹到新值。如果刚开始,这将使您难以适应,因为看起来可以更改值,但实际上您只是将变量名绑定到新值。这意味着原始范围仍将绑定到相同的值:

counter = 0
for i <- 1..10 do
  counter = counter + 1
end
IO.inspect counter # prints 0

另一种思考方式是,内部counter只是一个新变量,恰好与外部变量具有相同的名称。上面的示例等效于:

counter = 0
for i <- 1..10 do
  new_counter = counter + 1
end

要解决此问题,正如您已经正确观察到的,我们使用诸如Enum.reduce/2-3之类的函数,该函数使我们可以将中间结果存储在累加器中。因此,每个需要记住的变量都必须放入累加器中。然后,从匿名函数返回下一次迭代的值,该函数允许Enum.reduce将其传递回下一次迭代。

在您的情况下,这意味着您将要记住lensum,我们可以将它们放入一个元组中以{sum, len}的形式传递。减少量的基本结构应为:

result = Enum.reduce list, {0, len}, fn {sum, len} ->
  new_sum = sum + ...
  new_len = ...
  {new_sum, new_len}
end
{sum, _} = result
sum

如果您想更进一步,以更好地了解所有这些部分如何组合在一起,我强烈建议您阅读Programming Elixir by Dave Thomas的前几章。它包括一些练习,从头开始构建Enum.reduce之类的实用程序,对您大有帮助。

最后,在您的代码中仍有一些可以改进的地方。例如,它将很乐意接受无效数字,例如“ 2”。以此为灵感,我将如何解决这个问题:

string
|> String.graphemes
|> Enum.reverse
|> Enum.with_index
|> Enum.reduce(0, fn
  {"0", _}, acc -> acc
  {"1", i}, acc -> acc + :math.pow(2, i)
end)
|> trunc()

我已经听到一些声音在尖叫:“但是现在您要在列表上进行多次迭代”,我想回答的是,只要这不是瓶颈,就可以超越性能优化。如果确实发现需要从中抽出最后一点性能,通常最好编写一个根本不依赖Enum模块的尾递归函数,但我将保留它供您探索。

答案 2 :(得分:1)

按照Enum.reduce的方式进行操作并不是解决问题的最佳方法。

这是因为Enum.reduce的每次迭代都应“返回”累加器以进行下一次迭代。

当您放置acc = acc + temp * powerlen = len-1时,您并没有分配acclen来让您在下一次迭代中赋予它们的值,只是在当前的变量,实际上,您应该收到有关这些变量未被使用的警告。

在代码块的末尾,由于您要做的最后一件事是len = len - 1,并且由于len始终是相同的,因此传递给下一个迭代的内容始终是1,在输入"11"的情况下。

当您添加IO.puts/1时,最后将:okIO.puts的“返回”)传递给下一个迭代,这会产生错误:重新看到。

使用Enum.reduce的另一种方法可能是保留一个元组{len, sum},并在该块的末尾始终放置lensum的新值。 / p>

因此,您可以将其更改为:

def to_decimal(string) do
  list = String.graphemes(string)
  len = length(list)

  Enum.reduce(list, {0, len}, fn x, {sum, len} ->
    temp = String.to_integer(x)
    power = :math.pow(2, len - 1) |> round()
    sum = sum + temp * power
    len = len - 1

    {sum, len}
  end)
end

这将最后返回一个包含{n, l}的元组,其中n是十进制表示形式,而l是最终长度。