Bu diğer cevaplar biraz yanıltıcı. Bu eşitsizliği açıklayabilecek uygulama ayrıntılarını belirttiklerini kabul ediyorum, ancak vakayı abartıyorlar. Jmite tarafından doğru bir şekilde önerildiği gibi , işlev çağrıları / özyineleme uygulamalarının kırılması için uygulamaya yöneliktirler . Birçok dil döngüleri özyineleme yoluyla uygular, bu nedenle döngüler bu dillerde açıkça daha hızlı olmayacaktır. Özyineleme hiçbir şekilde teoride (her ikisi de uygulanabilir olduğunda) döngüden daha az verimli değildir. Özeti Guy Steele'in 1977 tarihli "Pahalı Prosedür Çağrısı" Efsanesine ya da Zararlı Sayılan Prosedür Uygulamalarına ya da Lambda: Ultimate GOTO'ya Debunking
Folklor, GOTO ifadelerinin "ucuz" olduğunu, prosedür çağrılarının "pahalı" olduğunu belirtir. Bu efsane büyük ölçüde kötü tasarlanmış dil uygulamalarının bir sonucudur. Bu mitin tarihsel büyümesi düşünülür. Bu efsaneyi çürüten hem teorik fikirler hem de mevcut bir uygulama tartışılmıştır. Prosedür çağrılarının sınırsız kullanımının büyük üslup özgürlüğüne izin verdiği gösterilmiştir. Özellikle, herhangi bir akış şeması, ekstra değişkenler tanıtılmadan "yapılandırılmış" bir program olarak yazılabilir. GOTO ifadesi ve prosedür çağrısı ile ilgili zorluk soyut programlama kavramları ve somut dil yapıları arasında bir çatışma olarak nitelendirilir.
"Soyut programlama kavramları ve beton dil yapıları arasında çatışma" çoğu teorik modeller, örneğin, türsüz olmasından görülebilir lambda hesabı , bir yığın yok . Tabii ki, yukarıdaki makalenin gösterdiği ve Haskell gibi özyineleme dışında yineleme mekanizması olmayan diller tarafından da gösterildiği gibi, bu çatışma gerekli değildir.
fix
fix f x = f (fix f) x
( λ x . M) N⇝ M[ N/ x ][ N/ x]xMN-⇝
Şimdi bir örnek verelim. Define fact
olarak
fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1
İşte fact 3
burada, kompaktlık g
için eşanlamlı olarak kullanacağım fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1))
, yani fact = g 1
. Bu benim argümanımı etkilemez.
fact 3
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6
Büyümenin olmadığı ve her yinelemenin aynı miktarda alana ihtiyaç duyduğu ayrıntılarına bile bakmadan şekilden görebilirsiniz. (Teknik olarak, kaçınılmaz olan ve bir while
döngü için de geçerli olan sayısal sonuç büyür .) Burada sınırsızca büyüyen "yığını" işaret etmek için meydan okuyorum.
Görünüşe göre lambda hesabının arketipik semantiği, yaygın olarak "kuyruk çağrısı optimizasyonu" olarak adlandırılan şeyi zaten yapıyor. Tabii ki burada hiçbir "optimizasyon" gerçekleşmiyor. Burada "normal" çağrıların aksine "kuyruk" çağrıları için özel bir kural yoktur. Bu nedenle, işlev çağrısı semantiğinin birçok soyut karakterizasyonunda olduğu gibi, kuyruk çağrısı "optimizasyonu" nun ne yaptığının "soyut" bir karakterizasyonunu vermek zordur, kuyruk çağrısı "optimizasyonu" için hiçbir şey yapılmaz!
fact
Birçok dilde "yığın taşması" nın benzer tanımının, bu diller tarafından işlev çağrısı semantiği doğru bir şekilde uygulanamamasıdır. (Bazı dillerin bir mazereti vardır.) Durum, bağlantılı listelerle diziler uygulayan bir dil uygulamasına kabaca benzemektedir. Bu tür "dizilere" endeksleme, dizilerin beklentisini karşılamayan bir O (n) işlemi olacaktır. Bağlantılı listeler yerine gerçek diziler kullanılan dilin ayrı bir uygulamasını yapmış olsaydım, "dizi erişim optimizasyonu" uyguladığımı söyleyemezsiniz, dizilerin bozuk bir uygulamasını düzelttiğimi söylerdiniz.
Yani Veedrac'ın cevabına cevap veriyor. Yığınlar özyineleme için "temel" değildir . Değerlendirme sırasında "yığın benzeri" davranış oluştukça, bu sadece döngülerin (yardımcı veri yapısı olmadan) ilk etapta uygulanamayacağı durumlarda olabilir! Başka bir deyişle, tam olarak aynı performans özelliklerine sahip özyineleme ile döngüler uygulayabilirim. Aslında, Şema ve SML'nin her ikisi de döngü yapıları içerir, ancak her ikisi de bunları özyineleme açısından tanımlar (ve en azından Şema'da, do
genellikle özyinelemeli çağrılara genişleyen bir makro olarak uygulanır .) Benzer şekilde Johan'ın cevabı için hiçbir şey derleyici Johan özyineleme için açıklanan derleme yaymak zorundadır. Aslında,döngüler veya özyineleme kullansanız da tam olarak aynı derleme. Derleyicinin, Johan'ın açıkladığı gibi, bir şekilde bir şekilde yaymak zorunda kaldığı tek zaman, yine de bir döngü tarafından ifade edilemeyen bir şey yaptığınız zamandır . Steele'nin makalesinde belirtildiği ve Haskell, Scheme ve SML gibi gerçek dillerin pratiği ile gösterildiği gibi, kuyruk çağrılarının "optimize edilebildiği" "son derece nadir" değildir, her zamanoptimize edilmelidir. Özyinelemenin belirli bir kullanımının sabit alanda çalışıp çalışmayacağı, nasıl yazıldığına bağlıdır, ancak bunu mümkün kılmak için uygulamanız gereken kısıtlamalar, sorununuzu bir döngü şekline sığdırmak için ihtiyaç duyacağınız kısıtlamalardır. (Aslında, daha az sıkıdırlar. Yardımcı makineleri gerektiren döngülerin aksine kuyruk çağrıları yoluyla daha temiz ve verimli bir şekilde işlenen durum makinelerini kodlama gibi sorunlar vardır.) Yine, tekrarlamanın daha fazla iş yapmasını gerektiren tek zaman kodunuz zaten bir döngü olmadığında.
Benim tahminim Johan kuyruk çağrısı "optimizasyonu" ne zaman gerçekleştireceği konusunda keyfi kısıtlamaları olan C derleyicilerinden söz ediyor. Johan ayrıca "yönetilen türlere sahip diller" hakkında konuşurken C ++ ve Rust gibi dillerden bahsediyor. C ++ 'dan gelen ve Rust'ta bulunan RAII deyimi, yüzeysel olarak kuyruk çağrılarına değil, kuyruk çağrılarına benzeyen şeyler yapar (çünkü "yıkıcılar" hala çağrılmalıdır). Kuyruk özyinelemesine izin verecek biraz farklı bir semantiğe kaydolmak için farklı bir sözdizimi kullanma önerileri olmuştur (yani daha önce yıkıcıları çağırınız)son kuyruk çağrısı ve "yıkılan" nesnelere erişime açıkça izin verilmiyor). (Çöp toplamanın böyle bir sorunu yoktur ve tüm Haskell, SML ve Şema çöp toplanan dillerdir.) Oldukça farklı bir şekilde, Smalltalk gibi bazı diller, birinci sınıf bir nesne olarak "yığını" ortaya koyarlar. "yığın" artık bir uygulama ayrıntısı değildir, ancak bu farklı semantiklerle ayrı arama türlerine sahip olmayı engellemez. (Java, güvenliğin bazı yönlerini ele alma biçimi nedeniyle yapılamayacağını söylüyor, ancak bu aslında yanlış .)
Uygulamada, işlev çağrılarının kırık uygulamalarının yaygınlığı üç ana faktörden gelir. İlk olarak, birçok dil, bozuk uygulamayı uygulama dilinden (genellikle C) devralır. İkincisi, deterministik kaynak yönetimi güzel ve konuyu daha karmaşık hale getiriyor, ancak sadece birkaç dil bunu sunuyor. Üçüncüsü ve benim tecrübelerime göre, çoğu insanın önem vermesinin nedeni, hata ayıklama amacıyla hatalar oluştuğunda yığın izleri istemeleridir. Sadece ikinci neden, potansiyel olarak teorik olarak motive edilebilen nedendir.