简化球拍功能

Simplifying a Racket function

我有以下功能 "change" 需要支付一定数量的钱,用于支付的 bill/coin 的大小,以及 returns 带有数字的列表"coins"($50、$20、$10、$5、$2 和 $1)人在完成交易后将收到:

(define (change total payment)
  (let [(x (- payment total))]
    (change-aux x '(50 20 10 5 2 1))))

(define (change-aux change coins)
  (cond
    [(empty? coins) empty]
    [else (let [(num-coins (floor (/ change (car coins))))]
            (append (list num-coins)
                    (change-aux (- change (* (car coins) num-coins)) (cdr coins))))]))

所以,如果我输入这些参数:

> (change 44 200)

它returns输出:

'(3 0 0 1 0 1)

即 200-44 = 156,对应 3 个硬币价值 50 美元,1 个硬币价值 5 美元,1 个硬币价值 1 美元。

我的问题是,是否有一种更优雅、更简化的方法来编写类似的过程而不依赖辅助函数,而是使用 lambda、filter、map、foldr、foldl 等?

提前致谢。

当然可以。

最终解决方案

(define (change total payment (coins '(50 20 10 5 2 1)))
  (let ((rest-diff (- payment total)))
    (map (lambda (coin)
           (let ((num-coins (floor (/ rest-diff coin))))
             (set! rest-diff (- rest-diff (* num-coins coin)))
             num-coins))
         coins)))

循序渐进

首先,使用innerdefine,可以从全局命名空间中去掉辅助函数

(define (change total payment)
  (define (change-aux change coins)
    (cond
      [(empty? coins) empty]
      [else (let [(num-coins (floor (/ change (car coins))))]
              (append (list num-coins)
                      (change-aux (- change (* (car coins) num-coins)) (cdr coins))))]))
  (let [(x (- payment total))]
    (change-aux x '(50 20 10 5 2 1))))

然后,您可以将辅助函数的一些变量拉入全局函数的 lambda 列表。

(define (change total payment (coins '(50 20 10 5 2 1)))
  (define (change-aux change) ;; eliminate coins in the inner lambda list
    (cond
      [(empty? coins) empty] ;; coins in function body looked up from outer arguments
      [else (let [(num-coins (floor (/ change (car coins))))]
              (append (list num-coins)
                      (change-aux (- change (* (car coins) num-coins)) (cdr coins))))]))
  (let [(x (- payment total))]
    (change-aux x))) ;; eliminate coins in the call

然后再看change-aux的代码,其实就是这么理解的 循环并尝试拟合当前值的最大倍数 剩下的差异 - 并收集这些结果。可以使用 map 循环并使用 set! 来改变其余部分。

(define (change total payment (coins '(50 20 10 5 2 1)))
  (let ((rest-diff (- payment total)))
    (map (lambda (coin)
           (let ((num-coins (floor (/ rest-diff coin))))
             (set! rest-diff (- rest-diff (* num-coins coin)))
             num-coins))
         coins)))

然后,你像上面那样调用:

(change 44 200)
;; '(3 0 0 1 0 1)

这是另一个 Lisp dialect 中的解决方案,它展示了如何在不对累加器变量进行任何突变的情况下使用左折叠(减少)来完成它,作为现有解决方案的一种功能对立点。

(defun change (amount coins)
  (reduce-left (tb ((counts . rem) next-coin)
                 (let* ((coin-count (floor rem next-coin))
                        (coin-value (* coin-count next-coin)))
                   (cons (cons coin-count counts)
                         (- rem coin-value))))
               coins
               (cons '() amount)))


3> (change 156 '(50 20 10 5 2 1))
((1 0 1 0 0 3) . 0)

4> (change 88 '(50 20 10 5 2 1))
((1 1 1 1 1 1) . 0)

请注意,这些值最终以相反的顺序报告并包装在一个额外的 cons 单元格中;可以围绕此 "plumbing" 使用 "porcelain" 函数以预期的形式报告结果。

想法是我们有一个累加器,如下所示:(counts . remainder)。存储在 car 中的 accumulator 的 counts 部分是到目前为止累积的硬币列表。当 reduce 完成后,这将保存最终列表。 cdr 字段保存了待处理的剩余金额;因为最后一枚硬币是 1,所以它总是出现为零。

使用这个累加器结构,我们处理硬币列表。

每次调用我们的 reduce 内核函数时,左边的参数是累加器,右边的参数 next-coin 是下一个硬币面额值。

我使用了一个叫做 tb ("tree bind") 宏的宏,它是一种 lambda 提供内置解构的宏,使它看起来像我们有三个参数.

reduce 作业的初始值是起始累加器,它有一个空的硬币列表,以及完整的原始数量:(cons nil amount)(为了更好的方案兼容性,我将其重写为 (cons '() amount) ).

reduce函数很简单:贪婪地计算下一个硬币值需要多少来表示余数,然后计算新的余数,将它们打包到一个新的累加器cons单元格中,即返回,并传递给函数的下一次调用,或者在处理硬币值列表时返回。

希望这指出了通往 "a more elegant, simplified way to write a similar procedure without relying on auxiliary functions, and rather use lambda, filter, map, foldr, foldl etc" 的道路,您可以在 Racket 中锻炼。祝你好运!