列表说明:折叠功能

时间:2014-11-10 22:56:47

标签: recursion functional-programming erlang tail-recursion fold

我越来越多地了解Erlang语言并且最近遇到了一些问题。我读到了foldl(Fun, Acc0, List) -> Acc1函数。我使用了learnyousomeerlang.com教程,并且有一个例子(例子是关于Erlang中的反向波兰表示法计算器):

%function that deletes all whitspaces and also execute
rpn(L) when is_list(L) ->
  [Res] = lists:foldl(fun rpn/2, [], string:tokens(L," ")),
  Res.

%function that converts string to integer or floating poitn value
read(N) ->
  case string:to_float(N) of
    %returning {error, no_float} where there is no float avaiable
    {error,no_float} -> list_to_integer(N);
    {F,_} -> F
  end.

%rpn managing all actions
rpn("+",[N1,N2|S]) -> [N2+N1|S];
rpn("-", [N1,N2|S]) -> [N2-N1|S];
rpn("*", [N1,N2|S]) -> [N2*N1|S];
rpn("/", [N1,N2|S]) -> [N2/N1|S];
rpn("^", [N1,N2|S]) -> [math:pow(N2,N1)|S];
rpn("ln", [N|S])    -> [math:log(N)|S];
rpn("log10", [N|S]) -> [math:log10(N)|S];
rpn(X, Stack) -> [read(X) | Stack].

据我所知lists:foldl对列表中的每个元素执行rpn/2。但就我能理解这个功能而言。我阅读了文档,但它对我没有多大帮助。有人能解释我lists:foldl的工作原理吗?

2 个答案:

答案 0 :(得分:7)

我们想说我们要在一起添加一个数字列表:

1 + 2 + 3 + 4.

这是一种非常正常的写作方式。但是我写了#34;在一起添加一个数字列表"而不是"在它们之间写下数字"。我在散文中表达操作的方式与我使用的数学符号之间存在根本的不同。我们这样做是因为我们知道它是一个等价的加法符号(因为它是可交换的),并且在我们的头脑中它立即减少到:

3 + 7.

然后

10.

那么最重要的是什么?问题是我们无法从这个例子中理解求和的概念。如果相反我写了"从0开始,然后一次从列表中取一个元素并将其作为运行总和添加到起始值&#34 ;?这实际上就是总和,而不是任意决定在减少等式之前先添加哪两项。

sum(List) -> sum(List, 0).

sum([], A)    -> A;
sum([H|T], A) -> sum(T, H + A).

如果你和我在一起,那么你已经准备好了解折叠。

上述功能存在问题;它太具体。它将三个想法拼凑在一起,而没有任何指定:

  • 迭代
  • 累积
  • 除了

很容易错过迭代和积累之间的差异,因为大多数时候我们从不给出第二个想法。大多数语言意外地鼓励我们错过差异,实际上,通过让相同的存储位置在每次类似函数的迭代中改变其值。

很容易因为在这个例子中写的方式而错过了加法的独立性,因为" +"看起来像"操作",而不是功能。

如果我说过"从1开始,然后一次从列表中取一个元素并乘以运行值&#34 ;?我们仍然会以完全相同的方式进行列表处理,但有两个例子可以比较,很明显乘法和加法是两者之间的唯一区别:

prod(List) -> prod(List, 1).

prod([], A)    -> A;
prod([H|T], A) -> prod(T, H * A).

这与执行流程完全相同,但对于内部操作和累加器的起始值。

因此,让我们将加法和乘法位转换为函数,这样我们就可以将模式的那部分拉出来:

add(A, B)  -> A + B.
mult(A, B) -> A * B.

我们怎样才能自己编写列表操作?我们需要传递函数 - 加法或乘法 - 并让它对值进行操作。此外,我们必须注意我们正在操作的事物的类型操作身份,否则我们将搞砸魔法这就是价值聚合。 "添加(0,X)"总是返回X,所以这个想法(0 + Foo)是添加标识操作。在乘法中,标识操作乘以1.因此我们必须启动我们的累加器为0加法和1加法(和建立列表空列表,依此类推)。所以我们不能用内置的累加器值来编写函数,因为它只适用于某些类型+操作对。

所以这意味着要编写一个折叠,我们需要有一个列表参数,一个执行参数的函数和一个累加器参数,如下所示:

fold([], _, Accumulator) ->
    Accumulator;
fold([H|T], Operation, Accumulator) ->
    fold(T, Operation, Operation(H, Accumulator)).

通过这个定义,我们现在可以使用这种更通用的模式编写sum / 1:

fsum(List) -> fold(List, fun add/2, 0).

还有prod / 1:

fprod(List) -> fold(List, fun prod/2, 1).

它们在功能上与我们上面写的相同,但符号更清晰,我们不必编写一堆递归细节,纠缠迭代的想法与增加或加法等特定操作的想法相关的想法。

在RPN计算器的情况下,聚合列表操作的想法与选择性分派的概念相结合(根据遇到/匹配的符号选择要执行的操作)。 RPN示例相对简单而且很小(您可以同时将所有代码放在头脑中,只需几行),但在您习惯功能范例之前,它所显示的过程会让您的头部受到伤害。在函数式编程中,只需基于列表操作和选择性调度,少量代码就可以创建一个任意复杂的不可预测(甚至不断演变!)行为的过程;这与今天更常见的其他范例中使用的条件检查,输入验证和程序检查技术非常不同。通过单个赋值和递归表示法来分析这种行为非常,因为每次迭代都是概念上独立的时间片,可以单独考虑系统的其余部分。我在基本问题之前谈了一点,但这是你可能希望考虑的一个核心思想,因为你考虑为什么我们喜欢使用折叠和递归符号等操作而不是程序性的,多指派循环。

我希望这不仅仅是困惑。

答案 1 :(得分:3)

首先,你必须记住haw作品rpn。如果要执行以下操作:2 * (3 + 5),您将使用输入"3 5 + 2 *"为函数提供信息。在您有25步输入程序的时候这非常有用:o)

第一个被调用的函数只是将这个字符列表拆分为元素:

1> string:tokens("3 5 + 2 *"," ").
["3","5","+","2","*"]
2>

然后它处理列表:foldl / 3。对于此列表的每个元素,使用输入列表的头部和当前累加器调用rpn / 2,并返回新的累加器。让我们一步一步走:

Step head  accumulator  matched rpn/2                           return value
1    "3"   []           rpn(X, Stack) -> [read(X) | Stack].    [3]
2    "5"   [3]          rpn(X, Stack) -> [read(X) | Stack].    [5,3]
3    "+"   [5,3]        rpn("+", [N1,N2|S]) -> [N2+N1|S];      [8]
4    "2"   [8]          rpn(X, Stack) -> [read(X) | Stack].    [2,8]
5    "*"   [2,8]        rpn("*",[N1,N2|S]) -> [N2*N1|S];       [16]

最后,lists:foldl/3返回与[16]匹配的[R],尽管rpn / 1返回R = 16