实用方案编程

发布于 2024-08-29 10:00:43 字数 1411 浏览 3 评论 0原文

自从我接触Scheme并决定使用Scheme实现一个命令行收入分区器以来已经有几个月了。

我最初的实现在延续上使用了简单的递归,但我认为延续会更适合这种类型的程序。如果有人(比我更熟练地使用Scheme)可以看看这个并提出改进建议,我将不胜感激。我认为多个 (display... 行也是使用宏的理想机会(我只是还没有接触到宏)。

(define (ab-income)
  (call/cc
   (lambda (cc)
     (let
         ((out (display "Income: "))
          (income (string->number (read-line))))
       (cond
         ((<= income 600)
          (display (format "Please enter an amount greater than $600.00~n~n"))
          (cc (ab-income)))
         (else
          (let
              ((bills    (* (/ 30 100) income))
               (taxes    (* (/ 20 100) income))
               (savings  (* (/ 10 100) income))
               (checking (* (/ 40 100) income)))
            (display (format "~nDeduct for bills:---------------------- $~a~n" (real->decimal-string bills 2)))
            (display (format "Deduct for taxes:---------------------- $~a~n" (real->decimal-string taxes 2)))
            (display (format "Deduct for savings:-------------------- $~a~n" (real->decimal-string savings 2)))
            (display (format "Remainder for checking:---------------- $~a~n" (real->decimal-string checking 2))))))))))

调用 (ab-venue) 要求输入,如果提供了低于 600 的值(根据我的理解),它会在 current-continuation 处返回 (ab-venue)。正如我之前所说)使用普通简递归也不错,但我认为如果值低于 600,则每次返回调用都会继续扩展函数

( 。如果这种担心不正确,请纠正我!)

It's been a few months since I've touched Scheme and decided to implement a command line income partitioner using Scheme.

My initial implementation used plain recursion over the continuation, but I figured a continuation would be more appropriate to this type of program. I would appreciate it if anyone (more skilled with Scheme than I) could take a look at this and suggest improvements. I'm that the multiple (display... lines is an ideal opportunity to use a macro as well (I just haven't gotten to macros yet).

(define (ab-income)
  (call/cc
   (lambda (cc)
     (let
         ((out (display "Income: "))
          (income (string->number (read-line))))
       (cond
         ((<= income 600)
          (display (format "Please enter an amount greater than $600.00~n~n"))
          (cc (ab-income)))
         (else
          (let
              ((bills    (* (/ 30 100) income))
               (taxes    (* (/ 20 100) income))
               (savings  (* (/ 10 100) income))
               (checking (* (/ 40 100) income)))
            (display (format "~nDeduct for bills:---------------------- $~a~n" (real->decimal-string bills 2)))
            (display (format "Deduct for taxes:---------------------- $~a~n" (real->decimal-string taxes 2)))
            (display (format "Deduct for savings:-------------------- $~a~n" (real->decimal-string savings 2)))
            (display (format "Remainder for checking:---------------- $~a~n" (real->decimal-string checking 2))))))))))

Invoking (ab-income) asks for input and if anything below 600 is provided it (from my understanding) returns (ab-income) at the current-continuation. My first implementation (as I said earlier) used plain-jane recursion. It wasn't bad at all either but I figured every return call to (ab-income) if the value was below 600 kept expanding the function.

(please correct me if that apprehension is incorrect!)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

筱果果 2024-09-05 10:00:43

首先,你不需要继续。根据标准,Scheme 将始终执行尾调用优化。尾调用是位于函数最终位置的函数调用;运行该调用后,不会发生任何其他事情。在这种情况下,我们不需要保留当前的激活记录;一旦我们调用的函数返回,我们就会弹出它。因此,尾调用重用当前的激活记录。举个例子,考虑一下:

(define (some-function x y)
  (preprocess x)
  (combine (modified x) y))
(some-function alpha beta)

当我们调用 some-function 时,我们在堆栈上为其激活记录分配空间:局部变量、参数等。然后我们调用 (preprocess x)< /代码>。由于我们需要返回 some-function 并继续处理,因此我们必须保留 some-function 的激活记录,因此我们为 some-function 推送一个新的激活记录代码>预处理。一旦返回,我们就会弹出预处理的堆栈帧并继续。接下来,我们需要评估modified;同样的事情也必须发生,当 modified 返回时,其结果将传递给 combine。人们可能会认为我们需要创建一个新的激活记录,运行 combine,然后将其返回到 some-function,但是 some-function > 不需要对该结果执行任何操作,只需返回它即可!因此,我们覆盖当前的激活记录,但保留返回地址;当combine返回时,它会将其值返回到等待它的值。这里,(combine (modified x) y) 是一个尾部调用,评估它不需要额外的激活记录。

这就是您在Scheme中实现循环的方式,例如:

(define (my-while cond body)
  (when (cond)
    (body)
    (my-while cond body)))

(let ((i 0))
  (my-while (lambda () (< i 10))
            (lambda () (display i) (newline) (set! i (+ i 1)))))

如果没有尾部调用优化,这将是低效的,并且可能会因长时间运行的循环构建大量对my-while的调用而溢出。然而,由于尾部调用优化,对 my-while cond body 的递归调用是一个跳转,并且不分配内存,使其与迭代一样高效。

其次,这里不需要任何宏。虽然您可以抽象出 display 块,但您可以使用普通函数来完成此操作。宏允许您在某种程度上更改语言的语法 - 添加您自己的定义,实现一些不评估其所有分支的类型大小写构造,等等。当然,仍然是 s 表达式,但语义不再是简单的“计算参数并调用函数”。然而,在这里,函数调用语义就足够了。

话虽如此,我认为这就是我实现代码的方式:

(require (lib "string.ss"))

(define (print-report width . nvs)
  (if (null? nvs)
    (void)
    (let ((name  (car  nvs))
          (value (cadr nvs)))
      (display (format "~a:~a $~a~n"
                       name
                       (make-string (- width (string-length name) 2) #\-)
                       (real->decimal-string value 2)))
      (apply print-report width (cddr nvs)))))

(define (ab-income)
  (display "Income: ")
  (let ((income (string->number (read-line))))
    (if (or (not income) (<= income 600)) 
      (begin (display "Please enter an amount greater than $600.00\n\n")
             (ab-income))
      (begin (newline)
             (print-report 40 "Deduct for bills"       (* 3/10 income)
                              "Deduct for taxes"       (* 2/10 income)
                              "Deduct for savings"     (* 1/10 income)
                              "Remainder for checking" (* 4/10 income))))))

首先,至少在我的 mzscheme 版本中,我需要一个 (require (lib "string.ss")) 行导入 real->decimal-string。接下来,我抽象出了您正在谈论的 display 块。我们看到的是,每一行都想在第40列以相同的格式打印钱,在其前面打印一个标签名称和一行破折号。因此,我编写了print-report。第一个参数是初始宽度;在本例中为 40。其余参数是字段值对。从宽度中减去每个字段的长度(加上冒号和空格的两个),我们生成一个由那么多破折号组成的字符串。我们使用 format 按正确的顺序排列字段,并使用 display 打印字符串。该函数对所有对进行递归(使用尾递归,因此我们不会破坏堆栈)。

在主函数中,我将 (display "Income: ") 移至 let; 之前。你忽略了它的结果,那为什么要把它赋给一个变量呢?然后,我增强了 if 条件来测试 input 是否为 false,当 string->number 无法解析输入时会发生这种情况。最后,我删除了你的局部变量,因为你所做的就是打印它们,并使用Scheme的分数语法而不是除法。 (当然,我使用 print-report 而不是 displayformat。)

我想就这些了;如果您对我所做的事情还有其他疑问,请随时询问。

First of all, you don't need a continuation. According to the standard, Scheme will always perform tail call optimization. A tail call is a function call which is in the final position in a function; after that call is run, nothing else will happen. In that situation, we don't need to preserve the activation record we're currently in; as soon as the function we call returns, we'll just pop it. Consequently, a tail call reuses the current activation record. As an example, consider this:

(define (some-function x y)
  (preprocess x)
  (combine (modified x) y))
(some-function alpha beta)

When we call some-function, we allocate space for its activation record on the stack: local variables, parameters, etc. We then call (preprocess x). Since we need to return to some-function and keep processing, we have to preserve some-function's activation record, and so we push a new activation record on for preprocess. Once that returns, we pop preprocess's stack frame and keep going. Next, we need to evaluate modified; the same thing has to happen, and when modified returns, its result is passed to combine. One would think we'd need to create a new activation record, run combine, and then return this to some-function—but some-function doesn't need to do anything with that result but return it! Thus, we overwrite the current activation record, but leave the return address alone; when combine returns, then, it will return its value to exactly what was waiting for it. Here, (combine (modified x) y) is a tail call, and evaluating it doesn't require an extra activation record.

This is how you can implement loops in Scheme, for instance:

(define (my-while cond body)
  (when (cond)
    (body)
    (my-while cond body)))

(let ((i 0))
  (my-while (lambda () (< i 10))
            (lambda () (display i) (newline) (set! i (+ i 1)))))

Without tail call optimization, this would be inefficient, and would potentially overflow with a long-running loop building up lots of calls to my-while. However, thanks to tail call optimization, the recursive call to my-while cond body is a jump, and allocates no memory, making this as efficient as iteration.

Secondly, you don't need any macros here. While you can abstract out the display block, you can do this with a plain function. Macros allow you, on some level, to change the syntax of the language—add your own sort of define, implement some type-case construct which doesn't evaluate all its branches, etc. Of course, it's all still s-expressions, but the semantics are no longer simply "evaluate the arguments and call the function". Here, however, function-call semantics are all you need.

With that said, I think this is how I'd implement your code:

(require (lib "string.ss"))

(define (print-report width . nvs)
  (if (null? nvs)
    (void)
    (let ((name  (car  nvs))
          (value (cadr nvs)))
      (display (format "~a:~a $~a~n"
                       name
                       (make-string (- width (string-length name) 2) #\-)
                       (real->decimal-string value 2)))
      (apply print-report width (cddr nvs)))))

(define (ab-income)
  (display "Income: ")
  (let ((income (string->number (read-line))))
    (if (or (not income) (<= income 600)) 
      (begin (display "Please enter an amount greater than $600.00\n\n")
             (ab-income))
      (begin (newline)
             (print-report 40 "Deduct for bills"       (* 3/10 income)
                              "Deduct for taxes"       (* 2/10 income)
                              "Deduct for savings"     (* 1/10 income)
                              "Remainder for checking" (* 4/10 income))))))

First, at least in my version of mzscheme, I needed a (require (lib "string.ss")) line to import real->decimal-string. Next, I abstracted out the display block you were talking about. What we see is that each line wants to print the money in the same format at the 40th column, printing a tag name and a row of dashes in front of it. Consequently, I wrote print-report. The first argument is the initial width; in this case, 40. The remaining arguments are field-value pairs. The length of each field (plus two for the colon and the space) are subtracted from the width, and we generate a string consisting of that many dashes. We use format to lay the fields out in the right order, and display to print the string. The function recurses over all the pairs (using tail recursion, so we won't blow the stack).

In the main function, I moved the (display "Income: ") to before the let; you ignore its result, so why assign it to a variable? I then augmented the if condition to test if input is false, which happens when string->number can't parse the input. Finally, I removed your local variables, since all you do is print them, and used Scheme's fraction syntax instead of division. (And of course, I use print-report instead of displays and formats.)

I think that's all; if you have any other questions about what I did, feel free to ask.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文