【问题标题】:Confused by "Init/Base" in foldr/foldl (Racket)被 foldr/foldl (Racket) 中的“Init/Base”弄糊涂了
【发布时间】:2015-11-16 02:36:38
【问题描述】:

我已经接近理解 foldrfoldl 但还没有完全理解。

我知道foldr 基本上是从“从右到左”在列表上执行某些功能的堆栈实现。

所以对于foldr

(define (fn-for-lon lon)
    (cond [(empty? lon) 0]
          [else
             (+ (first lon)
                (fn-for-lon (rest lon))]))

基本上相当于:

(foldr + 0 lon)

我知道foldl 基本上是尾递归的累加器版本,从“左到右”。

所以对于foldl

(define (fn-for-lon lon0)
    (local [(define (fn-for-lon lon acc)
                (cond [(empty? lon) acc]
                      [else
                          (fn-for-lon (rest lon) (+ acc (first lon)))]
   (fn-for-lon lon0)))

基本上相当于:

(foldl + 0 lon)

但是一旦你引入两个变量会发生什么?我试图阅读该主题,但我无法理解它,因为没有人谈论幕后发生的事情。

我真的很困惑第二个参数是“base”还是“init”,或者它只是取决于函数中是否采用了一个或两个变量。在我给出的 foldl 示例中,它似乎是 init acc 值(我想它最终是基本情况),但在 foldr 中它将是基本情况。是不是因为我只对proc使用了一个操作符?

(foldr (lambda (a b) (cons (add1 a) b)) empty (list 1 2 3 4))

像上面这样的事情我真的只是失去了所有的理解。我知道该怎么做,但不知道后台发生了什么。这使我在更复杂的问题中迷失了方向。

这是完全相同的东西吗?b 只是emptyb 是否临时替代(rest lon)

(define (fn-for-lon lon)
  (cond [(empty? lon) empty]
        [else
         (cons (add1 (first lon))
               (fn-for-lon (rest lon)))]))

【问题讨论】:

标签: scheme racket abstraction fold higher-order-functions


【解决方案1】:

foldlfoldr 的第二个参数始终是 init。如果您传入一个列表,您提供的函数必须始终采用 2 个参数,并且该函数的第二个参数是累积值(最初是 init,然后是上一次调用函数的返回值)。

(在您之前使用+ 的示例中,您可以认为它与(lambda (a b) (+ a b)) 相同。在这种情况下,a 是列表元素,b 是累积值。)

当您使用 N 个列表调用 foldlfoldr 时,您提供的函数必须采用 N+1 个参数;前 N 个参数对应于每个列表中的下一个元素,最后一个参数是累积值(最初是 init,然后是上一次调用函数的返回值)。


如果我提供了foldlfoldr 的(我自己的)实现,它会帮助您理解吗?他们在这里:

(define foldl
  (case-lambda
    ;; one list
    [(func init lst)
     (let loop ([result init]
                [rest lst])
       (if (null? rest)
           result
           (loop (func (car rest) result) (cdr rest))))]

    ;; multiple lists
    [(func init list1 . list2+)
     (let loop ([result init]
                [rests (cons list1 list2+)])
       (if (ormap null? rests)
           result
           (loop (apply func (append (map car rests) (list result)))
                 (map cdr rests))))]))

(define foldr
  (case-lambda
    ;; one list
    [(func init lst)
     (let recur ([rest lst])
       (if (null? rest)
           init
           (func (car rest) (recur (cdr rest)))))]

    ;; multiple lists
    [(func init list1 . list2+)
     (let recur ([rests (cons list1 list2+)])
       (if (ormap null? rests)
           init
           (apply func (append (map car rests)
                               (list (recur (map cdr rests)))))))]))

【讨论】:

    【解决方案2】:

    让我们看一下来自 Racket 的实际foldl

     (define foldl
        (case-lambda
          [(f init l)
           (check-fold 'foldl f init l null)
           (let loop ([init init] [l l])
             (if (null? l) init (loop (f (car l) init) (cdr l))))]
          [(f init l . ls)
           (check-fold 'foldl f init l ls)
           (let loop ([init init] [ls (cons l ls)])
             (if (pair? (car ls)) ; `check-fold' ensures all lists have equal length
                 (loop (apply f (mapadd car ls init)) (map cdr ls))
                 init))]))
    

    我们看到两种情况:第一种情况(f init l) 是为了提高效率。如果只使用一个列表l,那么我们会得到一个快速版本的foldl

    第二种情况(f init l . ls) 是您所追求的。在我们检查它之前,我们需要先看一下辅助函数mapadd

    调用(mapadd f l last) 会将f 应用于列表l 的所有元素,并将结果应用于`last。

    例子:

    > (mapadd sqr '(1 2 3 4) 42)
    '(1 4 9 16 42)
    

    mapadd的定义:

      (define (mapadd f l last)
        (let loop ([l l])
          (if (null? l)
            (list last)
            (cons (f (car l)) (loop (cdr l))))))
    

    回到foldl(f init l . ls) 案例。

    删除错误检查归结为

           (let loop ([init init] 
                      [ls (cons l ls)])
             (if (pair? (car ls))
                 (loop (apply f (mapadd car ls init)) 
                       (map cdr ls))
                 init))]))
    

    初始值init 绑定到变量(也称为)init,用于累积结果。变量ls 是当循环开始绑定到所有列表foldl 的列表时调用的。请注意,这些列表都具有相同的长度。循环继续,直到ls 中的所有列表都为空。测试(pair? (car ls)) 检查第一个列表是否为空,但请记住列表的长度相同。

    现在init 替换为(apply f (mapadd car ls init))。此调用首先获取每个列表的第一个元素,然后转换为 init 的当前值。然后应用f

    考虑以下示例:(foldl + 0 '(1 2) '(10 11)),其计算结果为 24。 这里

     f    = +
     init = 0
     ls   = ((1 2) (10 11))
    

    还有

     > (mapadd car '((1 2) (10 11)) 0)
     '(1 10 0)
    

    所以

     > (apply + (mapadd car '((1 2) (10 11)) 0))
     11
    

    下一轮我们会看到

     f    = +
     init = 11
     ls   = ((2) (11))
    

    (apply + (mapadd car ls init) 将评估为 24。

    解释示例(foldl + 0 '(1 2) '(10 11))的另一种方式。

    (define init 0)
    (for ([x (in-list '( 1  2))]              ; x and y loop in parallel
          [y (in-list '(10 11))])
      (set! init (apply + (list x y init))))  ; accumulate result in init
    init
    

    foldl 实现的复杂之处在于不知道使用了多少个列表。

    更新

    在实践中使用 foldl 时,最好根据其规范而非实现来考虑 foldl

    下面是使用两个列表调用 foldl 时的指定方式。

    电话:

      (foldl f x0
         (cons a0 (cons a1 (cons a2 '())))
         (cons b0 (cons b1 (cons b2 '()))))
    

    计算结果与

    相同
    (f a2 b2
       (f a1 b1
         (f a0 b0 
            x0)))
    

    会。

    我们可以试一试:

    >   (foldl (λ args (cons 'f args)) 'x0
           (cons 'a0 (cons 'a1 (cons 'a2 '())))
           (cons 'b0 (cons 'b1 (cons 'b2 '()))))
    '(f a2 b2 (f a1 b1 (f a0 b0 x0)))
    

    请注意,(λ args (cons 'f args)) 是仅在其参数列表中添加符号 f 的函数。

    【讨论】:

    • 好的,所以我认为基本上归结为我试图将定义融入我的老师给出的 foldr/foldl 的解释中,而实际上它们只是简化版本,旨在表明一个是尾巴隐居,一个不是。 base 不是基本情况,而是要在其上累积的基本初始值。
    • "基数不是基本情况,而是要在其上累加的基本初始值。"当场。
    • @AdamThompson 我添加了一条评论,说明如何根据其规范来考虑 foldl。尝试为foldr 制定类似的重写规则。
    【解决方案3】:

    折叠确实可以帮助我们,而不是让我们感到困惑。它们捕获某种递归模式以实现一致的重用。折叠是抽象定义的,也最容易抽象理解。了解你的语言的折叠实现细节的唯一原因是确保实现是有效的。

    只要我们有特定的递归模式(用等式伪代码编写),

    foo(xs) =
      matches xs with
         Empty -> zero
         Cons(head,tail) -> comb(head, 
                                    foo(tail))
    

    我们将列表的头部与 在列表尾部调用 foo 的递归结果相结合,我们得到了一个(正确的)折叠,并调用 @987654325 @ 与调用 foldr(comb, zero, xs) 相同。 如何在我们选择的语言中实现这一点并不重要。

    如您所见,组合函数(运算符或其他)必须接受 list 的“当前元素”作为其第一个参数,并将递归结果作为其 last。 仅当列表中没有更多元素时,使用zero值代替递归结果,初始化链上执行的comb计算从递归的基本案例Empty 返回。

    所以在阅读折叠定义时,我们总是将组合函数的第一个参数视为“当前元素”,心理上将最后一个参数视为“处理列表其余部分的递归结果”。

    为什么要对“当前”元素使用这种措辞?因为,假设我们的列表是[a,b,c,...,n],调用foldr(comb, z, [a,b,c,...,n]),按照上面的模式,和调用是一样的

    comb(a, 
          foldr(comb, z, [b,c,d,...,n]))
    ==
    comb(a,
       comb(b,
          comb(c, 
             ......, comb(n, z)......)))
    

    这个,在某种意义上,就是列表右折叠的定义,也就是列表catamorphism

    Racket 增加了同时折叠多个列表的能力。自然,组合函数的参数排列保持不变——除最后一个之外的所有参数都对应于当前元素,每个参数都来自每个参数列表;最后一个参数是 recursive result 将它们结合起来。

    一个相似但不同的模式是

    bar(xs) =
      matches xs with
         Empty -> zero
         Cons(head,tail) -> comb(head, 
                                    tail, 
                                       bar(tail))
    

    这被称为 paramorphism(参见 Common Lisp 中的maplist vs. mapcar)。

    由于两者都捕获递归,zero 对应于我们递归的归纳数据定义的基本情况:

    List of 'a =  Empty 
               or Cons('a, List of 'a)
    

    从基本情况开始,我们得到一个双重模式,即在递归调用之前建立一个值,a.o.t。跟上面一样,跟

    baz(zero, xs) =
      matches xs with
         Empty -> zero
         Cons(head,tail) -> baz( comb(head, zero),
                                    tail)
    

    您将识别为左折叠(并且zero 不再是基本情况值,而是在旅途中构建的值,或累积,并且最终在@时返回987654340@ 案例被击中)。

    所以这只是 (1+(2+(3+...(n+0)...)))(...(((0+1)+2)+3)...+n) 之间的区别strong>,左边是(为了表示方便,参数顺序颠倒显示)。当(+) 是一个关联操作时,一个结果将与另一个结果相同。对于数字,它是。

    另见this answer

    【讨论】:

      猜你喜欢
      • 2022-01-23
      • 2019-01-12
      • 1970-01-01
      • 2014-08-14
      • 2016-02-16
      • 1970-01-01
      • 1970-01-01
      • 2017-07-24
      • 2011-08-02
      相关资源
      最近更新 更多