指定されたコードには非テール位置のアプリケーションがあるため、その名前にもかかわらず、関数は反復的ではありません。:)
これを試して:
(define (fringe-tail tree)
(define (iter tree k)
(cond
[(null? tree)
(k '())]
[(not (pair? tree))
(k (list tree))]
[else
(iter (car tree)
(lambda (v1)
(iter (cdr tree)
(lambda (v2)
(k (append v1 v2))))))]))
(iter tree (lambda (a-fringe) a-fringe)))
ただし、それでも最初の引数の長さと同じくらい高価なappendを使用します。フリンジとフリンジテールへの特定の縮退入力は、多くの計算上の問題を引き起こします。
そのような縮退した入力の例を挙げましょう:
(define (build-evil-struct n)
(if (= n 0)
(list 0)
(list (list (build-evil-struct (sub1 n)))
(build-evil-struct (sub1 n))
(list n))))
(define evil-struct (build-evil-struct 20))
フリンジとフリンジイターの両方に適用すると、パフォーマンスが非常に悪くなります。私は、フリンジとフリンジテールについて、自分のシステムで数秒の計算時間を観察しています。これらのテストは、デバッグを無効にしてDrRacketで実行されました。デバッグを有効にすると、数値が大幅に異なります。
> (time (void (fringe evil-struct)))
cpu time: 2600 real time: 2602 gc time: 1212
> (time (void (fringe-tail evil-struct)))
cpu time: 4156 real time: 4155 gc time: 2740
これらの両方で、appendを使用すると、これらが特定の縮退入力の影響を受けやすくなります。フリンジの累積バージョンを作成すると、一定時間の短所操作を使用できるようになるため、そのコストを削減できます。
(define (fringe/acc tree)
(define (iter tree acc)
(cond [(null? tree)
acc]
[(not (pair? tree))
(cons tree acc)]
[else
(iter (car tree) (iter (cdr tree) acc))]))
(iter tree '()))
この構造でのフリンジ/accのパフォーマンスを見てみましょう。
> (time (void (fringe/acc evil-struct)))
cpu time: 272 real time: 274 gc time: 92
ずっといい!そして、ここですべての呼び出しを末尾呼び出しに変えるのは簡単なことです。
(define (fringe/acc/tail tree)
(define (iter tree acc k)
(cond [(null? tree)
(k acc)]
[(not (pair? tree))
(k (cons tree acc))]
[else
(iter (cdr tree) acc
(lambda (v1)
(iter (car tree) v1 k)))]))
(iter tree '() (lambda (v) v)))
> (time (void (fringe/acc/tail evil-struct)))
cpu time: 488 real time: 488 gc time: 280
この特定のケースでは、Racketのスタックの実装は、継続で表す具体化されたスタックよりも少し速いため、フリンジ/accはフリンジ/acc/tailよりも高速です。それでも、これらは両方とも、追加を回避するため、フリンジよりも大幅に優れています。
このすべてが言われています:この関数は、フラット化関数としてすでにラケットに組み込まれています!したがって、車輪の再発明をしたくない場合は、それを使用する方がよいでしょう。:)