【问题标题】:Explanation of lists:fold function列表解释:折叠功能
【发布时间】:2015-01-07 09:26:36
【问题描述】:

我对 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 的工作原理吗?

【问题讨论】:

  • 这个问题确实比 Erlang 更笼统,并且触及了不熟悉函数式范式的程序员的常见绊脚石。阅读这个特定示例的便利性让我想知道如果以更一般的方式重新表述这个问题是否会对更多人有用。

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


【解决方案1】:

假设我们想将一个数字列表相加:

1 + 2 + 3 + 4.

这是一种很正常的写法。但是我写的是“将数字列表加在一起”,而不是“写数字之间的加号”。我用散文表达运算的方式和我使用的数学符号之间存在根本的不同。我们这样做是因为我们知道它是加法的等价符号(因为它是可交换的),并且在我们的头脑中它立即归约为:

3 + 7.

然后

10.

那么有什么大不了的?问题是我们无法从这个例子中理解 summation 的概念。如果我写的是“从 0 开始,然后一次从列表中取出一个元素并将其作为运行总和添加到起始值中”怎么办?这实际上是求和的意义所在,在等式被约简之前,它并不是随意决定先添加哪两件事。

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

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

如果到目前为止你和我在一起,那么你已经准备好理解折叠了。

上面的函数有问题; 太具体了。它将三个想法编织在一起,没有单独指定任何一个:

  • 迭代
  • 积累
  • 补充

很容易忽略迭代和积累之间的区别,因为大多数时候我们从不考虑这一点。大多数语言不小心鼓励我们忽略差异,实际上是通过让相同的存储位置在每次迭代类似函数时更改其值。

仅仅因为这个例子中的写法,很容易忽略加法的独立性,因为“+”看起来像一个“操作”,而不是一个函数。

如果我说“从 1 开始,然后一次从列表中取出一个元素并将其乘以运行值”会怎样?我们仍然会以完全相同的方式进行列表处理,但是通过两个示例进行比较,很明显乘法和加法是两者之间的唯一区别:

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.

我们如何自己编写列表操作?我们需要传递一个函数——加法或乘法——并让它对值进行操作。另外,我们必须注意我们正在操作的事物的类型操作身份,否则我们会搞砸魔法那就是价值聚合。 "add(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 示例相对简单且小(您可以一次将所有代码放在脑海中,只需几行代码),但在您习惯函数式范例之前,它所体现的过程可能会让您头疼。在函数式编程中,仅基于列表操作和选择性分派,少量代码就可以创建一个任意复杂的不可预测(甚至演变!)行为的过程;这与今天更常见的其他范例中使用的条件检查、输入验证和程序检查技术非常不同。单次赋值和递归表示法极大地帮助分析这种行为,因为每次迭代都是概念上独立的时间片,可以将其与系统的其余部分隔离开来。我说的比基本问题提前了一点,但这是您在考虑为什么我们喜欢使用诸如折叠和递归符号之类的操作而不是程序性、多重性时可能希望考虑的核心思想赋值循环。

我希望这不仅能帮助你解决困惑。

【讨论】:

    【解决方案2】:

    首先,您必须记住 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 返回与[R] 匹配的[16],尽管 rpn/1 返回R = 16

    【讨论】:

    • 这是对 foldl + rpn 案例的一个很好的逐步说明。我希望在两者之间(我的将军,这个具体的)OP 找到他的“啊,哈!”时刻。
    猜你喜欢
    • 2014-01-15
    • 1970-01-01
    • 2011-01-22
    • 2014-05-18
    • 1970-01-01
    • 1970-01-01
    • 2019-09-10
    • 2015-06-26
    • 2012-08-31
    相关资源
    最近更新 更多