5

私が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-income)は入力を要求し、600未満のものが提供された場合、(私の理解から)それはに戻り(ab-income)ますcurrent-continuation。私の最初の実装(前に言ったように)は、プレーンジェーン再帰を使用しました。まったく悪くはありませんでしたが(ab-income)、値が600未満の場合は、関数を拡張し続けた場合のすべてのリターン呼び出しを計算しました。

(その懸念が間違っている場合は私を訂正してください!)

4

1 に答える 1

17

まず第一に、あなたは継続を必要としません。標準によれば、Schemeは常に末尾呼び出しの最適化を実行します。末尾呼び出しは、関数の最後の位置にある関数呼び出しです。その呼び出しが実行された後、他には何も起こりません。そのような状況では、現在のアクティベーションレコードを保持する必要はありません。呼び出す関数が返されるとすぐに、それをポップします。したがって、末尾呼び出しは現在のアクティブ化レコードを再利用します。例として、これを考えてみましょう。

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

を呼び出すときはsome-function、スタック上のアクティブ化レコード(ローカル変数、パラメーターなど)にスペースを割り当てます。次に、を呼び出します(preprocess x)。に戻ってsome-function処理を続ける必要があるためsome-function、のアクティベーションレコードを保持する必要があります。そのため、新しいアクティベーションレコードをにプッシュしpreprocessます。preprocessそれが戻ったら、スタックフレームをポップして続行します。次に、評価する必要があり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できますが、単純な関数を使用してこれを行うことができます。マクロを使用すると、あるレベルで、言語の構文を変更できます。独自の種類を追加したり、defineすべてのブランチを評価しない活字ケース構造を実装したりできます。もちろん、すべて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する行が必要でした。次に、あなたが話していたブロックを抽象化しました。私たちが見ているのは、各行が40列目に同じ形式でお金を印刷し、その前にタグ名とダッシュの行を印刷したいということです。その結果、私は書いた。最初の引数は初期幅です。この場合、。残りの引数はフィールドと値のペアです。各フィールドの長さ(コロンとスペースの場合は2を加えたもの)が幅から差し引かれ、その数のダッシュで構成される文字列が生成されます。フィールドを正しい順序で配置し、文字列を印刷するために使用します。関数はすべてのペアで再帰します(末尾再帰を使用するため、スタックをブローしません)。(require (lib "string.ss"))real->decimal-stringdisplayprint-report40formatdisplay

main関数で、 ;の(display "Income: ")前に移動しました。let結果を無視するのに、なぜそれを変数に割り当てるのですか?次に、条件を拡張して、falseifかどうかをテストしました。これは、入力を解析できない場合に発生します。最後に、ローカル変数を削除しました。これは、ローカル変数を出力するだけで、除算の代わりにSchemeの分数構文を使用したためです。(そしてもちろん、私はsとsの代わりに使用します。)inputstring->numberprint-reportdisplayformat

それだけだと思います。私がしたことについて他に質問があれば、遠慮なく質問してください。

于 2010-04-17T18:44:39.457 に答える