Lisp öğrenmeye başlarken kuyruk özyinelemeli terimiyle karşılaştım . Tam olarak ne anlama geliyor?
Lisp öğrenmeye başlarken kuyruk özyinelemeli terimiyle karşılaştım . Tam olarak ne anlama geliyor?
Yanıtlar:
İlk N doğal sayıyı toplayan basit bir işlevi düşünün. (örneğin sum(5) = 1 + 2 + 3 + 4 + 5 = 15
).
İşte özyineleme kullanan basit bir JavaScript uygulaması:
function recsum(x) {
if (x === 1) {
return x;
} else {
return x + recsum(x - 1);
}
}
Aradıysanız recsum(5)
, JavaScript yorumlayıcı bunu değerlendirir:
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15
JavaScript yorumlayıcısı toplamı hesaplama işini yapmaya başlamadan önce her yinelemeli çağrının nasıl tamamlanması gerektiğini unutmayın.
İşte aynı işlevin kuyruk özyinelemeli sürümü:
function tailrecsum(x, running_total = 0) {
if (x === 0) {
return running_total;
} else {
return tailrecsum(x - 1, running_total + x);
}
}
İşte tailrecsum(5)
( tailrecsum(5, 0)
varsayılan ikinci argüman nedeniyle etkili olur) çağırırsanız gerçekleşecek olaylar dizisi .
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
Kuyruk özyinelemeli durumda, özyinelemeli çağrının her değerlendirmesi running_total
ile güncellenir.
Not: Orijinal cevap Python'dan örnekler kullanmıştır. Bunlar JavaScript olarak değiştirildi, çünkü Python yorumlayıcıları kuyruk çağrısı optimizasyonunu desteklemiyor . Ancak, kuyruk çağrısı optimizasyonu ECMAScript 2015 spesifikasyonunun bir parçası olsa da , çoğu JavaScript tercümanı bunu desteklemez .
tail recursion
Kuyruk çağrılarını optimize etmeyen bir dilde nasıl başarılabileceğime kafam karıştı .
Gelen geleneksel özyineleme , tipik modeli öncelikle tekrarlanan aramalara gerçekleştirmek ve sonra özyinelemeli çağrısının dönüş değeri alıp sonucu hesaplamak olmasıdır. Bu şekilde, her özyinelemeli çağrıdan dönene kadar hesaplamanızın sonucunu alamazsınız.
Gelen kuyruk özyineleme , öncelikle hesaplamalar ve sonra bir sonraki özyinelemeli adıma mevcut adımın sonuçlarını geçen özyinelemeli çağrı yürütün. Bu son ifadenin şeklinde olur (return (recursive-function params))
. Temel olarak, herhangi bir özyinelemeli adımın dönüş değeri, bir sonraki özyinelemeli çağrının dönüş değeri ile aynıdır .
Bunun sonucu, bir sonraki özyinelemeli adımı gerçekleştirmeye hazır olduğunuzda, artık geçerli yığın çerçevesine ihtiyacınız kalmamasıdır. Bu, bazı optimizasyonlara izin verir. Aslında, uygun bir şekilde yazılmış bir derleyici ile, asla kuyruk tekrarlayan çağrı ile yığın taşması keskin nişancısı olmamalıdır . Bir sonraki özyinelemeli adım için mevcut yığın çerçevesini yeniden kullanmanız yeterlidir. Eminim Lisp bunu yapar.
Önemli bir nokta, kuyruk özyinelemesinin esasen döngüye eşdeğer olmasıdır. Bu sadece derleyici optimizasyonu değil, ifadeyle ilgili temel bir gerçek. Bu iki yöne de gider: formun herhangi bir döngüsünü alabilirsiniz
while(E) { S }; return Q
Burada E
ve Q
ifadelerdir ve S
bir ifade dizisidir ve onu kuyruk yinelemeli işleve dönüştürür
f() = if E then { S; return f() } else { return Q }
Tabii ki, E
, S
ve Q
bazı değişkenler üzerinde bazı ilginç değerini hesaplamak için tanımlanmak zorundadır. Örneğin, döngü işlevi
sum(n) {
int i = 1, k = 0;
while( i <= n ) {
k += i;
++i;
}
return k;
}
kuyruk özyinelemeli işlev (ler) e eşdeğerdir
sum_aux(n,i,k) {
if( i <= n ) {
return sum_aux(n,i+1,k+i);
} else {
return k;
}
}
sum(n) {
return sum_aux(n,1,0);
}
(Kuyruk özyinelemeli fonksiyonun daha az parametreye sahip bir fonksiyonla bu "sarılması" yaygın bir fonksiyonel deyimdir.)
else { return k; }
değiştirilebilirreturn k;
Lua'da Programlama kitabından yapılan bu alıntı, uygun bir kuyruk özyinelemesinin nasıl yapılacağını gösterir (Lua'da, ancak Lisp için de geçerli olmalıdır) ve neden daha iyi olduğunu gösterir.
Bir kuyruk çağrısı [kuyruk özyineleme] bir çağrı olarak giyinmiş bir tür gotodur. Kuyruk çağrısı, bir işlev son eylemi olarak başka bir işlev çağırdığında gerçekleşir; Örneğin, aşağıdaki kodda, çağrı
g
bir kuyruk çağrısıdır:function f (x) return g(x) end
f
Çağrılardan sonrag
, başka bir şey yapmaz. Bu gibi durumlarda, çağrılan işlev sona erdiğinde programın çağrı işlevine dönmesi gerekmez. Bu nedenle, kuyruk çağrısından sonra, programın çağırma işlevi hakkında yığındaki herhangi bir bilgiyi tutması gerekmez. ...Düzgün bir kuyruk çağrısı yığın alanı kullanmadığından, bir programın yapabileceği "iç içe" kuyruk çağrılarının sayısında bir sınır yoktur. Örneğin, argüman olarak herhangi bir sayı ile aşağıdaki işlevi çağırabiliriz; hiçbir zaman yığını taşmaz:
function foo (n) if n > 0 then return foo(n - 1) end end
... Daha önce söylediğim gibi, bir kuyruk çağrısı bir çeşit goto. Bu nedenle, Lua'da uygun kuyruk çağrılarının oldukça yararlı bir uygulaması, durum makinelerini programlamak içindir. Bu tür uygulamalar her durumu bir fonksiyonla temsil edebilir; durumu değiştirmek, belirli bir işleve gitmek (veya çağırmak) içindir. Örnek olarak, basit bir labirent oyunu ele alalım. Labirent, her biri dört kapıya kadar çeşitli odalara sahiptir: kuzey, güney, doğu ve batı. Her adımda, kullanıcı bir hareket yönüne girer. Bu yönde bir kapı varsa, kullanıcı ilgili odaya gider; aksi takdirde program bir uyarı yazdırır. Amaç başlangıç odasından son odaya gitmek.
Bu oyun, mevcut odanın durum olduğu tipik bir durum makinesidir. Bu labirenti her oda için bir işlevle uygulayabiliriz. Bir odadan diğerine geçmek için kuyruk çağrıları kullanıyoruz. Dört odalı küçük bir labirent şöyle görünebilir:
function room1 () local move = io.read() if move == "south" then return room3() elseif move == "east" then return room2() else print("invalid move") return room1() -- stay in the same room end end function room2 () local move = io.read() if move == "south" then return room4() elseif move == "west" then return room1() else print("invalid move") return room2() end end function room3 () local move = io.read() if move == "north" then return room1() elseif move == "east" then return room4() else print("invalid move") return room3() end end function room4 () print("congratulations!") end
Gördüğünüz gibi, özyinelemeli bir çağrı yaptığınızda:
function x(n)
if n==0 then return 0
n= n-2
return x(n) + 1
end
Bu özyineli özyinelemeli değildir, çünkü özyinelemeli çağrı yapıldıktan sonra bu işlevde hala yapılacak işler (1 ekleyin) vardır. Çok yüksek bir sayı girerseniz, büyük olasılıkla yığın taşmasına neden olur.
Düzenli özyineleme kullanarak, her özyinelemeli çağrı, çağrı yığınına başka bir giriş gönderir. Özyineleme tamamlandığında, uygulama daha sonra her girişi tamamen geri almalıdır.
Kuyruk özyineleme ile, dile bağlı olarak derleyici yığını bir girişe daraltabilir, böylece yığın alanından tasarruf edebilirsiniz ... Büyük bir özyinelemeli sorgu aslında bir yığın taşmasına neden olabilir.
Temel olarak Kuyruk özyinelemeleri yinelemeye göre optimize edilebilir.
Jargon dosyası, kuyruk özyineleme tanımı hakkında şunları söyleyecektir:
kuyruk özyineleme /n./
Zaten hasta değilseniz, kuyruk yinelemesine bakın.
Bunu kelimelerle açıklamak yerine, bir örnek. Bu faktöriyel fonksiyonun bir Şema versiyonudur:
(define (factorial x)
(if (= x 0) 1
(* x (factorial (- x 1)))))
İşte kuyruk özyinelemeli faktöriyel bir sürümü:
(define factorial
(letrec ((fact (lambda (x accum)
(if (= x 0) accum
(fact (- x 1) (* accum x))))))
(lambda (x)
(fact x 1))))
İlk versiyonda, özyinelemeli çağrının çarpma ifadesine beslendiğini ve dolayısıyla özyinelemeli çağrı yaparken durumun yığına kaydedilmesi gerektiğini fark edeceksiniz. Kuyruk özyinelemeli versiyonda, özyinelemeli çağrının değerini bekleyen başka bir S ifadesi yoktur ve yapılacak başka bir iş olmadığından, durumun yığına kaydedilmesi gerekmez. Kural olarak, Şema kuyruk yinelemeli işlevleri sabit yığın alanı kullanır.
list-reverse
prosedürü sabit yığın uzayında çalışır, ancak öbek üzerinde bir veri yapısı oluşturur ve büyütür. Bir ağaç geçişi ek bir argümanda simüle edilmiş bir yığın kullanabilir. vb.
Kuyruk özyineleme özyinelemeli çağrının özyinelemeli algoritmada son mantık komutunda son olduğunu belirtir.
Genellikle özyinelemede, özyinelemeli çağrıları durduran ve çağrı yığınını açmaya başlayan bir temel durum vardır. Klasik bir örnek kullanmak için, Lisp'den daha fazla C-ish olsa da, faktöriyel fonksiyon kuyruk yinelemesini göstermektedir. Yinelemeli çağrı , temel durum durumunu kontrol ettikten sonra gerçekleşir .
factorial(x, fac=1) {
if (x == 1)
return fac;
else
return factorial(x-1, x*fac);
}
Faktöriğe ilk çağrı factorial(n)
burada olacaktır fac=1
(varsayılan değer) ve n faktöriyelin hesaplanacağı sayıdır.
else
, "temel durum" olarak adlandırabileceğiniz, ancak birkaç satıra yayılan adımdır. Seni yanlış mı anladım yoksa varsayım doğru mu? Kuyruk özyineleme sadece bir gömlek için iyidir?
factorial
Örnek hepsi bu, sadece klasik basit bir örnektir.
Bu, talimat işaretçisini yığının üzerine itmek yerine, özyinelemeli bir işlevin üstüne atlayıp yürütmeye devam edebileceğiniz anlamına gelir. Bu, fonksiyonların yığını taşmadan süresiz olarak geri çekilmesini sağlar.
Konu üzerine, yığın çerçevelerinin neye benzediğine dair grafik örnekleri olan bir blog yazısı yazdım .
İşte iki işlevi karşılaştıran bir hızlı kod pasajı. Birincisi, belirli bir sayının faktöriyelini bulmak için geleneksel özyineleme. İkincisi kuyruk özyineleme kullanır.
Anlamak çok basit ve sezgisel.
Yinelemeli bir işlevin kuyruk yinelemeli olup olmadığını anlamanın kolay bir yolu, temel durumda somut bir değer döndürüp döndürmediğidir. Yani 1 veya doğru ya da bunun gibi bir şey döndürmez. Büyük olasılıkla yöntem parametrelerinden birinin bazı varyantlarını döndürür.
Başka bir yol, özyinelemeli çağrının herhangi bir ekleme, aritmetik, modifikasyon, vb.
public static int factorial(int mynumber) {
if (mynumber == 1) {
return 1;
} else {
return mynumber * factorial(--mynumber);
}
}
public static int tail_factorial(int mynumber, int sofar) {
if (mynumber == 1) {
return sofar;
} else {
return tail_factorial(--mynumber, sofar * mynumber);
}
}
Anlamamın en iyi yolu tail call recursion
, son çağrının (veya kuyruk çağrısının) işlevin kendisi olduğu özel bir özyineleme durumudur .
Python'da sağlanan örnekleri karşılaştırmak:
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
^ özyineleme
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
^ KUYRUK TEKRARI
Genel özyinelemeli sürümde görebileceğiniz gibi, kod bloğundaki son çağrı x + recsum(x - 1)
. Yani recsum
yöntemi çağırdıktan sonra , başka bir işlem var x + ..
.
Bununla birlikte, kuyruk özyinelemeli versiyonda, kod bloğundaki son çağrı (veya kuyruk çağrısı) tailrecsum(x - 1, running_total + x)
, yöntemin kendisine son çağrı yapıldığı ve bundan sonra hiçbir işlem olmadığı anlamına gelir.
Bu nokta önemlidir, çünkü burada görüldüğü gibi kuyruk özyineleme belleği büyütmemektedir, çünkü alttaki VM kendini kuyruk pozisyonunda (bir fonksiyonda değerlendirilecek son ifade) çağıran bir fonksiyon gördüğünde, mevcut yığın çerçevesini ortadan kaldırır. Kuyruk Çağrısı Optimizasyonu (TCO) olarak bilinir.
NB. Yukarıdaki örneğin çalışma zamanı TCO'yu desteklemeyen Python'da yazıldığını unutmayın. Bu sadece konuyu açıklamak için bir örnek. TCO, Scheme, Haskell vb. Dillerde desteklenmektedir
Java'da, Fibonacci işlevinin olası bir kuyruk yinelemeli uygulaması:
public int tailRecursive(final int n) {
if (n <= 2)
return 1;
return tailRecursiveAux(n, 1, 1);
}
private int tailRecursiveAux(int n, int iter, int acc) {
if (iter == n)
return acc;
return tailRecursiveAux(n, ++iter, acc + iter);
}
Bunu standart özyinelemeli uygulama ile karşılaştırın:
public int recursive(final int n) {
if (n <= 2)
return 1;
return recursive(n - 1) + recursive(n - 2);
}
iter
için acc
ne zaman iter < (n-1)
.
Bir Lisp programcı değilim, ama sanırım bu yardımcı olacaktır.
Temel olarak, yinelemeli çağrı yaptığınız son şey olacak şekilde bir programlama tarzıdır.
İşte kuyruk özyineleme kullanarak faktöriyeller yapan bir Ortak Lisp örneği. Yığınsız doğa nedeniyle, insan delicesine büyük faktöriyel hesaplamalar yapabilir ...
(defun ! (n &optional (product 1))
(if (zerop n) product
(! (1- n) (* product n))))
Ve sonra eğlence için deneyebilirsin (format nil "~R" (! 25))
Kısacası, bir kuyruk özyineleme özyinelemeli çağrıyı beklemek zorunda kalmamak için işlevdeki son ifade olarak özyinelemeli çağrıyı içerir.
Yani bu bir kuyruk özyineleme, yani N (x - 1, p * x), derleyicinin bir for döngüye (faktöriyel) optimize edilebileceğini anlamak için akıllı olduğu fonksiyondaki son ifadedir. İkinci parametre p, ara ürün değerini taşır.
function N(x, p) {
return x == 1 ? p : N(x - 1, p * x);
}
Bu, yukarıdaki faktöryel işlevi yazmanın kuyruk özyinelemesiz yoludur (bazı C ++ derleyicileri yine de optimize edebilir).
function N(x) {
return x == 1 ? 1 : x * N(x - 1);
}
ama bu değil:
function F(x) {
if (x == 1) return 0;
if (x == 2) return 1;
return F(x - 1) + F(x - 2);
}
" Kuyruk özyineleme anlama - Visual Studio C ++ - montaj görünümü " başlıklı uzun bir yazı yazdım
burada tailrecsum
daha önce bahsedilen fonksiyonun Perl 5 versiyonudur .
sub tail_rec_sum($;$){
my( $x,$running_total ) = (@_,0);
return $running_total unless $x;
@_ = ($x-1,$running_total+$x);
goto &tail_rec_sum; # throw away current stack frame
}
Kuyruk özyineleme ile ilgili Bilgisayar Programlarının Yapısı ve Yorumundan bir alıntıdır .
Yinelemenin ve özyinelemenin aksine, özyinelemeli bir süreç kavramını özyinelemeli bir prosedürle karıştırmamaya dikkat etmeliyiz. Bir prosedürü özyinelemeli olarak tanımladığımızda, prosedür tanımının (doğrudan veya dolaylı olarak) prosedürün kendisine atıfta bulunduğu sözdizimsel gerçeğe atıfta bulunuyoruz. Ancak bir süreci, örneğin doğrusal olarak özyinelemeli bir paterni izleyerek tanımladığımızda, bir prosedürün nasıl yazıldığının sözdiziminden değil, sürecin nasıl geliştiğinden bahsediyoruz. Gerçekleştirici gibi yinelemeli bir yordamı yinelemeli bir süreç oluşturmak olarak adlandırmamız rahatsız edici görünebilir. Ancak süreç gerçekten yinelemelidir: Durumu tamamen üç durum değişkeni tarafından yakalanır ve bir tercümanın işlemi gerçekleştirmek için sadece üç değişkeni takip etmesi gerekir.
Süreç ve prosedür arasındaki ayrımın kafa karıştırıcı olabilmesinin bir nedeni, ortak diller (Ada, Pascal ve C dahil) uygulamalarının çoğunun, özyinelemeli prosedürlerin yorumlanmasının, prensip olarak, tekrarlanan işlem yinelemeli olsa bile, prosedür çağrılarının sayısı. Sonuç olarak, bu diller yinelemeli süreçleri ancak do, repeat, until, for ve while gibi özel amaçlı “döngü yapılarına” başvurarak tanımlayabilir. Şema'nın uygulanması bu kusuru paylaşmaz. Yinelemeli işlem özyinelemeli bir yordamla tanımlanmış olsa bile, sabit alanda yinelemeli bir işlem yürütür. Bu özelliğe sahip bir uygulamaya kuyruk yinelemeli denir. Kuyruk özyinelemeli bir uygulama ile, yineleme sıradan prosedür çağrı mekanizması kullanılarak ifade edilebilir, böylece özel yineleme yapıları sadece sözdizimsel şeker olarak faydalıdır.
Özyinelemeli işlev, kendi kendine çağrılan bir işlevdir
Programcıların minimum miktarda kod kullanarak verimli programlar yazmasına olanak tanır .
Dezavantajı, düzgün yazılmadığı takdirde sonsuz döngülere ve diğer beklenmedik sonuçlara neden olabilmeleridir .
Hem Basit Yinelemeli işlevi hem de Kuyruk Yinelemeli işlevini açıklayacağım
Basit bir özyinelemeli işlev yazmak için
Verilen örnekten:
public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
Yukarıdaki örnekten
if(n <=1)
return 1;
Döngüden ne zaman çıkılacağına karar veren faktör mü
else
return n * fact(n-1);
Gerçek işlem yapılacak mı?
Kolay anlaşılması için görevi tek tek kırmama izin verin.
Eğer koşarsam dahili olarak neler olduğunu görelim fact(4)
public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
If
döngü başarısız olur, böylece döngü olur, böylece else
geri döner4 * fact(3)
Yığın hafızasında, 4 * fact(3)
İkame n = 3
public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
If
o gider böylece döngü başarısız else
döngü
böylece geri dönüyor 3 * fact(2)
Unutmayın `` 4 * gerçek (3) ''
İçin çıktı fact(3) = 3 * fact(2)
Şimdiye kadar yığın 4 * fact(3) = 4 * 3 * fact(2)
Yığın hafızasında, 4 * 3 * fact(2)
İkame n = 2
public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
If
o gider böylece döngü başarısız else
döngü
böylece geri dönüyor 2 * fact(1)
Aradığımızı hatırla 4 * 3 * fact(2)
İçin çıktı fact(2) = 2 * fact(1)
Şimdiye kadar yığın 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
Yığın hafızasında, 4 * 3 * 2 * fact(1)
İkame n = 1
public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
If
döngü doğru
böylece geri dönüyor 1
Aradığımızı hatırla 4 * 3 * 2 * fact(1)
İçin çıktı fact(1) = 1
Şimdiye kadar yığın 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Son olarak, gerçeğin sonucu (4) = 4 * 3 * 2 * 1 = 24
Kuyruk Özyineleme olurdu
public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
If
döngü başarısız olur, böylece döngü olur, böylece else
geri dönerfact(3, 4)
Yığın hafızasında, fact(3, 4)
İkame n = 3
public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
If
o gider böylece döngü başarısız else
döngü
böylece geri dönüyor fact(2, 12)
Yığın hafızasında, fact(2, 12)
İkame n = 2
public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
If
o gider böylece döngü başarısız else
döngü
böylece geri dönüyor fact(1, 24)
Yığın hafızasında, fact(1, 24)
İkame n = 1
public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
If
döngü doğru
böylece geri dönüyor running_total
İçin çıktı running_total = 24
Son olarak, gerçeğin sonucu (4,1) = 24
Kuyruk özyineleme şu anda yaşadığınız hayattır. Aynı yığın çerçevesini sürekli olarak geri dönüştürürsünüz, çünkü bir "önceki" çerçeveye geri dönmek için hiçbir neden veya araç yoktur. Atılmak için geçmiş bitti ve bitti. Süreciniz kaçınılmaz olarak ölünceye kadar sonsuza dek geleceğe doğru giden bir çerçeve elde edersiniz.
Bazı işlemlerin ek kareler kullanabileceğini düşündüğünüzde analoji bozulur, ancak yığın sonsuza kadar büyümezse yine de kuyruk yinelemeli olarak kabul edilir.
Kuyruk özyineleme, özyinelemeli çağrının dönüşünden sonra hiçbir hesaplama yapılmadığı işlevin sonunda ("kuyruk") kendisini çağırdığı özyinelemeli bir işlevdir. Birçok derleyici, özyinelemeli bir çağrıyı kuyruk özyinelemeli veya yinelemeli bir çağrıyla değiştirmeyi en iyi duruma getirir.
Bir sayının hesaplama faktöriyelini düşünün.
Basit bir yaklaşım şöyle olacaktır:
factorial(n):
if n==0 then 1
else n*factorial(n-1)
Faktöriyel (4) dediğinizi varsayalım. Özyineleme ağacı:
factorial(4)
/ \
4 factorial(3)
/ \
3 factorial(2)
/ \
2 factorial(1)
/ \
1 factorial(0)
\
1
Yukarıdaki durumda maksimum özyineleme derinliği O (n) 'dir.
Ancak, aşağıdaki örneği göz önünde bulundurun:
factAux(m,n):
if n==0 then m;
else factAux(m*n,n-1);
factTail(n):
return factAux(1,n);
Gerçekler için özyineleme ağacı (4) şöyle olur:
factTail(4)
|
factAux(1,4)
|
factAux(4,3)
|
factAux(12,2)
|
factAux(24,1)
|
factAux(24,0)
|
24
Burada da, maksimum özyineleme derinliği O (n) 'dir, ancak çağrıların hiçbiri yığına ekstra değişken eklemez. Böylece derleyici bir yığınla uzaklaşabilir.
Kuyruk Özyineleme normal özyineleme ile karşılaştırıldığında oldukça hızlıdır. Hızlıdır, çünkü ataların çağrısının çıktısı, izi tutmak için yığına yazılmaz. Ancak normal özyinelemede tüm atalar yolu tutmak için yığına yazılan çıktıyı çağırır.
Bir kuyruk özyinelemeli fonksiyon dönmeden önce öyle son işlem özyinelemeli fonksiyon çağrısı yapmak bir özyinelemeli fonksiyonudur. Yani, özyinelemeli işlev çağrısının dönüş değeri hemen döndürülür. Örneğin, kodunuz şöyle görünür:
def recursiveFunction(some_params):
# some code here
return recursiveFunction(some_args)
# no code after the return statement
Kuyruk çağrısı optimizasyonu veya kuyruk çağrısı eliminasyonu uygulayan derleyiciler ve yorumlayıcılar, yığın taşmalarını önlemek için yinelemeli kodu optimize edebilir. Derleyiciniz veya yorumcunuz kuyruk çağrısı optimizasyonu (CPython yorumlayıcısı gibi) uygulamıyorsa, kodunuzu bu şekilde yazmanın başka bir yararı yoktur.
Örneğin, bu Python'da standart bir özyinelemeli faktöryel işlevdir:
def factorial(number):
if number == 1:
# BASE CASE
return 1
else:
# RECURSIVE CASE
# Note that `number *` happens *after* the recursive call.
# This means that this is *not* tail call recursion.
return number * factorial(number - 1)
Ve bu faktöriyel fonksiyonun bir kuyruk çağrısı özyinelemeli versiyonu:
def factorial(number, accumulator=1):
if number == 0:
# BASE CASE
return accumulator
else:
# RECURSIVE CASE
# There's no code after the recursive call.
# This is tail call recursion:
return factorial(number - 1, number * accumulator)
print(factorial(5))
(Bu Python kodu olmasına rağmen, CPython yorumlayıcısının kuyruk çağrısı optimizasyonu yapmadığını unutmayın, bu nedenle kodunuzu bu şekilde düzenlemek çalışma zamanı avantajı sağlamaz.)
Faktöriyel örnekte gösterildiği gibi, kuyruk çağrısı optimizasyonunu kullanmak için kodunuzu biraz daha okunmaz hale getirmeniz gerekebilir. (Örneğin, temel durum artık biraz sezgisel değildir ve accumulator
parametre bir tür küresel değişken olarak etkili bir şekilde kullanılmaktadır.)
Ancak kuyruk çağrı optimizasyonunun yararı, yığın taşması hatalarını önlemesidir. (Yinelenen bir algoritma yerine yinelemeli bir algoritma kullanarak da aynı avantajı elde edeceğinizi not edeceğim.)
Yığın taşmaları, çağrı yığını çok fazla çerçeve nesnesinin üzerine itildiğinde ortaya çıkar. Bir işlev çağrıldığında çerçeve nesnesi çağrı yığınına itilir ve işlev döndüğünde çağrı yığınından çıkar. Çerçeve nesneleri, yerel değişkenler ve işlev döndüğünde hangi kod satırının döndürüleceği gibi bilgiler içerir.
Yinelemeli işleviniz geri dönmeden çok fazla yinelemeli çağrı yaparsa, çağrı yığını çerçeve nesnesi sınırını aşabilir. (Sayı platforma göre değişir; Python'da varsayılan olarak 1000 çerçeve nesnesidir.) Bu, bir yığın taşmasına neden olur . (Hey, bu web sitesinin adı buradan geliyor!)
Ancak, özyinelemeli işlevinizin yaptığı son şey, özyinelemeli çağrı yapmak ve dönüş değerini döndürmekse, geçerli kare nesnesinin çağrı yığınında kalması için gerek yoktur. Sonuçta, özyinelemeli işlev çağrısından sonra kod yoksa, geçerli çerçeve nesnesinin yerel değişkenlerine takılmak için bir neden yoktur. Böylece mevcut kare nesnesinden çağrı yığınında tutmak yerine derhal kurtulabiliriz. Bunun sonucu, çağrı yığınınızın boyutunun büyümemesi ve bu nedenle taşmayı yığılamamasıdır.
Bir derleyici veya yorumlayıcı, kuyruk çağrısı optimizasyonunun ne zaman uygulanabileceğini tanıyabilmesi için kuyruk çağrısı optimizasyonuna sahip olmalıdır. O zaman bile, kuyruk çağrısı optimizasyonunu kullanmak için özyinelemeli işlevinizdeki kodu yeniden düzenleyebilirsiniz ve okunabilirlikteki bu potansiyel düşüşün optimizasyona değip değmeyeceği size bağlıdır.
Kuyruk çağrısı özyineleme ve kuyruk çağrısı özyineleme arasındaki temel farklardan bazılarını anlamak için bu tekniklerin .NET uygulamalarını araştırabiliriz.
İşte C #, F # ve C ++ \ CLI'de bazı örneklere sahip bir makale: C #, F # ve C ++ \ CLI'de Kuyruk Özyineleme Maceraları .
C # kuyruk çağrısı özyineleme için optimize etmezken F # yapar.
İlke arasındaki farklar döngülere karşı Lambda hesabıdır. C #, döngüler göz önünde bulundurularak tasarlanırken F #, Lambda hesabı ilkelerinden oluşturulmuştur. Lambda hesabının ilkeleri hakkında çok iyi (ve ücretsiz) bir kitap için bkz . Abelson, Sussman ve Sussman'ın Bilgisayar Programlarının Yapısı ve Yorumu .
F # 'daki kuyruk çağrıları ile ilgili olarak, çok iyi bir tanıtım makalesi için bkz . F #' daki Kuyruk Çağrılarına Detaylı Giriş . Son olarak, kuyruk olmayan özyineleme ve kuyruk çağrısı özyineleme (F # 'da) arasındaki farkı kapsayan bir makale: F keskinliğinde kuyruk özyineleme ve kuyruk olmayan özyineleme .
C # ve F # arasındaki kuyruk çağrısı özyinelemesinin tasarım farklılıklarından bazılarını okumak isterseniz, bkz . C # ve F #'da Kuyruk Çağrısı Opcode'u Oluşturma .
Hangi koşulların C # derleyicisinin kuyruk çağrısı optimizasyonları gerçekleştirmesini engellediğini bilmek istiyorsanız, bu makaleye bakın: JIT CLR kuyruk çağrısı koşulları .
İki temel özyineleme türü vardır: kafa özyineleme ve kuyruk özyineleme.
In kafa özyineleme , bir işlevi özyinelemeli çağrı yapar ve sonra belki örneğin özyinelemeli çağrısının sonucunu kullanarak, biraz daha hesaplamalar gerçekleştirir.
Bir de kuyruk özyinelemeli fonksiyon, bütün hesaplamalar ilk gerçekleşmesi ve özyinelemeli çağrı olur son şeydir.
Alındığı bu süper harika yazı. Lütfen okumayı düşünün.
Özyineleme, kendisini çağıran bir işlev anlamına gelir. Örneğin:
(define (un-ended name)
(un-ended 'me)
(print "How can I get here?"))
Kuyruk-Özyineleme işlevi sona erdiren özyineleme anlamına gelir:
(define (un-ended name)
(print "hello")
(un-ended 'me))
Bakın, sonlanmamış fonksiyon (Şema jargonundaki prosedür) kendisini çağırmaktır. Başka bir (daha yararlı) örnek:
(define (map lst op)
(define (helper done left)
(if (nil? left)
done
(helper (cons (op (car left))
done)
(cdr left))))
(reverse (helper '() lst)))
Yardımcı prosedürde, sol nil değilse yaptığı LAST şey kendini çağırmaktır (bir şey eksilerini ve cdr bir şey SONRA). Temel olarak bir listeyi nasıl eşlediğinizdir.
Kuyruk özyineleme, yorumlayıcının (veya dile ve satıcıya bağlı derleyicinin) optimize edebilmesi ve bir while döngüsüne eşdeğer bir şeye dönüştürebilmesi için büyük bir avantaja sahiptir. Aslında, Şema geleneğinde, çoğu "için" ve "while" döngüsü kuyruk-özyineleme tarzında yapılır (bildiğim kadarıyla ve süre için yoktur).
Bu sorunun birçok harika yanıtı var ... ama yardım edemem ama "kuyruk özyineleme" veya en azından "uygun kuyruk özyineleme" nin nasıl tanımlanacağına dair alternatif bir yaklaşımla karşılaşıyorum. Yani: bir programdaki belirli bir ifadenin özelliği olarak bakmalı mıdır? Yoksa buna bir programlama dilinin uygulanmasının bir özelliği olarak bakılmalı mıdır?
İkinci görünümde daha fazla bilgi için, Will Clinger tarafından "Uygun Kuyruk Özyineleme ve Alan Verimliliği" (PLDI 1998) adlı klasik bir makale , "uygun kuyruk özyinelemesini" bir programlama dili uygulamasının bir özelliği olarak tanımlamıştır. Tanım, bir kişinin uygulama ayrıntılarını göz ardı etmesine izin vermek için yapılandırılmıştır (çağrı yığınının gerçekte çalışma zamanı yığını üzerinden mi yoksa yığınla ayrılmış bağlantılı bir çerçeve listesi aracılığıyla mı temsil edildiği gibi).
Bunu başarmak için asimptotik analiz kullanır: program yürütme süresinin genellikle gördüğü gibi değil, program alanı kullanımını kullanır . Bu şekilde, bir yığın çalışma ayrılmış bağlantı listesinin bir çalışma zamanı çağrı yığınına karşı alan kullanımı asimptotik olarak eşdeğer olur; bu yüzden programlama dili uygulama detayını (uygulamada kesinlikle biraz önemli olan ancak belirli bir uygulamanın "mülkiyet kuyruğu özyinelemeli" gereksinimini karşılayıp karşılamadığını belirlemeye çalıştığında suları biraz çamurlayabilen bir detay göz ardı edilir. )
Bu makale çeşitli nedenlerden dolayı dikkatle incelenmeye değer:
Bir programın kuyruk ifadelerinin ve kuyruk çağrılarının endüktif bir tanımını verir . (Böyle bir tanım ve bu tür çağrıların neden önemli olduğu, burada verilen diğer cevapların çoğunun konusu gibi görünmektedir.)
İşte bu tanımlar, sadece metnin bir lezzetini sağlamak için:
Tanım 1 kuyruk ifadeler Çekirdek Şema yazılmış bir program tanımlandığı tümevarımsal olarak izler.
- Bir lambda ifadesinin gövdesi bir kuyruk ifadesidir
- Eğer
(if E0 E1 E2)
her iki, bir kuyruk ifadesidirE1
veE2
kuyruk ifadelerdir.- Başka hiçbir şey bir kuyruk ifadesi değildir.
Tanım 2 bir kuyruk arama bir prosedür çağrı kuyruk ifadesidir.
(bir kuyruk özyinelemeli çağrı veya makalenin dediği gibi "kendi kuyruğu çağrısı", prosedürün kendisinin çağrıldığı bir kuyruk çağrısının özel bir durumudur.)
Çekirdek Şemasını değerlendirmek için altı farklı "makine" için, her bir makinenin içinde bulunduğu asimtotik uzay karmaşıklık sınıfı dışında aynı gözlemlenebilir davranışa sahip olduğu resmi tanımlamalar sağlar .
Örneğin, sırasıyla 1. yığın tabanlı bellek yönetimi, 2. çöp toplama ancak kuyruk çağrıları yok, 3. çöp toplama ve kuyruk çağrıları olan makineler için tanımlar verdikten sonra, kağıt daha gelişmiş depolama yönetimi stratejileriyle devam eder. 4. "evlis kuyruk özyineleme", burada bir kuyruk çağrısında son alt ifade argümanının değerlendirilmesi boyunca çevrenin korunmasına ihtiyaç duyulmadığı, 5. bir kapama ortamının o kapamanın sadece serbest değişkenlerine indirgenmesi ve 6. Appel ve Shao tarafından tanımlanan "alan için güvenli" anlambilim .
Makinelerin aslında altı farklı uzay karmaşıklık sınıfına ait olduğunu kanıtlamak için, karşılaştırılan her bir makine çifti için kağıt, bir makinede asimtotik alan patlamasını açığa çıkaracak, diğerinde değil programların somut örneklerini sunmaktadır.
(Şimdi cevabımı okurken, Clinger belgesinin önemli noktalarını gerçekten yakalayıp yakalamadığımı bilmiyorum. Ancak, ne yazık ki, şu anda bu cevabı geliştirmek için daha fazla zaman ayıramıyorum.)
Birçok kişi burada özyinelemeyi zaten açıkladı. Riccardo Terrell'in “.NET'te Eşzamanlılık, Eşzamanlı ve paralel programlamanın modern modelleri” kitabından özyinelemenin sağladığı bazı avantajlar hakkında birkaç düşünceden bahsetmek istiyorum:
“Fonksiyonel özyineleme FP'de yinelemenin doğal yoludur, çünkü durum mutasyonunu önler. Her yineleme sırasında, döngü yapıcısına güncellenmek (değiştirilmek) yerine yeni bir değer iletilir. Buna ek olarak, programınızı daha modüler hale getiren ve paralellikten faydalanma fırsatlarını tanıtan, yinelemeli bir işlev oluşturulabilir. "
İşte aynı kitaptan kuyruk özyineleme hakkında bazı ilginç notlar:
Kuyruk çağrısı özyineleme, düzenli bir özyinelemeli işlevi herhangi bir risk ve yan etki olmadan büyük girdileri işleyebilen optimize edilmiş bir sürüme dönüştüren bir tekniktir.
NOT Bir kuyruk çağrısının optimizasyon olarak temel nedeni veri yerini, bellek kullanımını ve önbellek kullanımını geliştirmektir. Kuyruk çağrısı yaparak, arayan kişi arayanla aynı yığın alanını kullanır. Bu, bellek basıncını azaltır. Aynı bellek sonraki arayanlar için yeniden kullanıldığından ve yeni bir önbellek hattına yer açmak için daha eski bir önbellek satırını çıkarmak yerine önbellekte kalabileceğinden önbelleği marjinal olarak iyileştirir.