Fonksiyonel diller özyinelemede daha mı iyi?


41

TL; DR: İşlevsel diller özyinelemeyi işlevsel olmayan dillerden daha iyi idare ediyor mu?

Şu anda Kod Tamamlandı 2'yi okuyorum. Kitaptaki bir noktada yazar bizi özyineleme konusunda uyarıyor. Mümkün olduğunda kaçınılması gerektiğini ve özyineleme kullanan işlevlerin genellikle döngü kullanan bir çözümden daha az etkili olduğunu söylüyor. Örnek olarak, yazar böyle bir sayının faktörünü hesaplamak için özyinelemeyi kullanarak bir Java işlevi yazdı (şu anda benimle kitabım olmadığı için tam olarak aynı olmayabilir):

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

Bu kötü bir çözüm olarak sunulmaktadır. Bununla birlikte, fonksiyonel dillerde, özyinelemeyi kullanmak genellikle işleri yapmanın tercih edilen yoludur. Örneğin, özyinelemeyi kullanarak Haskell'deki faktöriyel fonksiyon şudur:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

Ve yaygın olarak iyi bir çözüm olarak kabul edilir. Gördüğüm gibi, Haskell özyinelemeyi çok sık kullanıyor ve üzerine kaşlarını çattığını hiçbir yerde görmedim.

Yani sorum temelde:

  • İşlevsel diller özyinelemeyi işlevsel olmayan dillerden daha mı iyi yapıyor?

EDIT: Kullandığım örneklerin sorumu açıklamak için en iyi olmadığının farkındayım. Sadece Haskell'in (ve genel olarak işlevsel dillerin) özyinelemeyi işlevsel olmayan dillerden daha sık kullandığını belirtmek istedim.


10
Örnek olarak: pek çok işlevsel dil kuyruk çağrısı optimizasyonunu yoğun olarak kullanırken, çok az işlemsel dil bunu yapar. Bu, kuyruk çağrısı özyinelemesinin bu işlevsel dillerde daha ucuz olduğu anlamına gelir .
Joachim Sauer

7
Aslında verdiğin Haskell tanımı oldukça kötü. factorial n = product [1..n]daha özlü, daha verimli ve yığın için büyük taşma yapmaz n(ve eğer not almanız gerekiyorsa, tamamen farklı seçenekler gerekir). productBazı açısından tanımlanmaktadır foldki, bir yinelemeli tanımlanmış, ancak son derece dikkatli. Özyineleme olduğunu çoğu zaman kabul edilebilir bir çözüm, ancak bunu yanlış / Suboptimal yapmak kolay hala.

1
@JoachimSauer - Küçük bir süsleme ile, yorumunuz değerli bir cevap verecektir.
Mark Booth

Düzenlemeniz sürüklenmemi yakalamadığınızı gösteriyor. Verdiğiniz tanım , işlevsel dillerde bile kötü olan mükemmel bir özyineleme örneğidir . Alternatifim ayrıca özyinelemeli (bir kütüphane işlevinde olsa da) ve çok verimli, ancak özyinelemenin nasıl bir fark yarattığını. Haskell, aynı zamanda tembellik normal kuralları ihlal ettiğinde garip bir durumdur (örnek olay: nokta: fonksiyonlar kuyruk özyinelemeli iken istif taşabilir ve kuyruk özyinelemeden çok verimli olabilir).

@delnan: Açıklama için teşekkürler! Düzenlememi düzenleyeceğim;)
marco-fiset

Yanıtlar:


36

Evet, yapıyorlar, ama sadece yapabildikleri için değil, mecbur oldukları için de .

Buradaki anahtar kavram saflık : saf işlev, yan etkisi olmayan ve durumu olmayan bir işlevdir. İşlevsel programlama dilleri genel olarak, kod hakkında muhakeme ve bariz olmayan bağımlılıklardan kaçınma gibi birçok nedenden dolayı saflığı içerir. Bazı diller, en önemlisi Haskell bile, yalnızca saf koda izin verecek kadar ileri gider ; Bir programın sahip olabileceği herhangi bir yan etki (örneğin, G / Ç gerçekleştirme), dili saf kılan, saf olmayan bir çalışma zamanına taşınır.

Yan etkilere sahip olmamanız, döngü sayaçlarına sahip olamayacağınız anlamına gelir (çünkü bir döngü sayacı değiştirilebilir durumu oluşturur ve bu durumu değiştirmek bir yan etki olur), bu nedenle saf bir işlevsel dilin alabileceği en yinelemeli bir listeyi yinelemektir ( bu işleme genellikle denir foreachveya map). Bununla birlikte, özyineleme, saf işlevsel programlama ile doğal bir eşleşmedir - (salt okunur) fonksiyon argümanları ve (sadece-yaz) geri dönüş değeri dışında tekrarlanmaya gerek yoktur.

Bununla birlikte, yan etkilere sahip olmamak aynı zamanda özyinelemenin daha verimli bir şekilde uygulanabileceği ve derleyicinin daha agresif bir şekilde optimize edebileceği anlamına gelir. Bu tür bir derleyiciyi kendim üzerinde daha önce incelemedim, ancak söyleyebildiğim kadarıyla, çoğu işlevsel programlama dilinin derleyicileri kuyruk çağrısı optimizasyonu gerçekleştiriyor ve hatta bazı özyinelemeli yapıları sahnelerin arkasındaki döngüler halinde derleyebiliyor.


2
Kayıt için, kuyruk çağrısı ortadan kaldırılması saflığa dayanmaz.
Fularlı,

2
@scarfridge: Tabii ki değil. Ancak, saflık verildiğinde, bir derleyicinin kuyruk çağrılarına izin vermek için kodunuzu yeniden sıralaması çok daha kolaydır.
tdammers

GCC, TCC'yi GHC'den çok daha iyi bir iş çıkarır, çünkü bir thunk yaratırken TCO yapamazsınız.
dan_waterworth

18

Yinelemeyle özyinelemeyi karşılaştırıyorsun. Kuyruk çağrısı ortadan kaldırması olmadan , yineleme gerçekten daha verimlidir çünkü fazladan bir işlev çağrısı yoktur. Ayrıca, yineleme sonsuza dek devam edebilir, oysa çok fazla işlev çağrısından yığın alanı tükenmek mümkündür.

Ancak, yineleme bir sayaç değiştirmeyi gerektirir. Bu , tamamen işlevsel bir ortamda yasaklanmış değişken bir değişken olması gerektiği anlamına gelir . Bu nedenle fonksiyonel diller, yineleme işlemine gerek kalmadan çalışmak için özel olarak tasarlanmıştır, bu nedenle akıcı işlev çağırır.

Fakat bunların hiçbiri, kod numaranızın neden bu kadar şık olduğunu göstermiyor. Örneğiniz, kalıp eşleştirme olan farklı bir özellik gösterir . Bu yüzden Haskell örneğinde kesin şartlamalar yoktur. Başka bir deyişle, kodunuzu küçük yapan basitleştirilmiş özyineleme değildir; desen eşleşmesi.


Desen eşleşmesinin ne demek olduğunu zaten biliyorum ve kullandığım dillerde özlediğim Haskell'de harika bir özellik olduğunu düşünüyorum!
marco-fiset

@ marcof Demek istediğim yinelemeye karşı yinelemeyle ilgili tüm konuşmalar kod numaranızın şıklığını ele almıyor. Bu gerçekten koşullarla vs desen eşleştirme hakkında. Belki de cevabımı üstüme koymalıydım.
Chrisaycock

Evet, ben de anladım: P
marco-fiset

@chrisaycock: Yinelemeyi, döngü gövdesinde kullanılan tüm değişkenlerin hem argüman hem de özyinelemeli çağrıların geri dönüş değerleri olduğu kuyruk özyinelemesi olarak görmek mümkün müdür?
Giorgio

@Giorgio: Evet, fonksiyonunuzun aynı tipte bir demet almasını sağlayın.
Ericson2314

5

Teknik olarak hayır, fakat pratik olarak evet.

Soruna fonksiyonel bir yaklaşım kullanırken özyineleme çok daha yaygındır. Bu nedenle, işlevsel bir yaklaşımı kullanmak için tasarlanan diller genellikle yinelemeyi daha kolay / daha iyi / daha az sorunlu yapan özellikler içerir. Başımın üstünden üç ortak var:

  1. Kuyruk Çağrı Optimizasyonu. Diğer posterlerde belirtildiği gibi, işlevsel diller genellikle TCO gerektirir.

  2. Tembel Değerlendirme. Haskell (ve birkaç başka dil) tembelce değerlendirilir. Bu, bir yöntemin gerçek “çalışmasını” gerekli olana kadar erteler. Bu daha özyinelemeli veri yapılarına ve dolayısıyla, onlar üzerinde çalışmak için özyinelemeli yöntemlere yol açma eğilimindedir.

  3. Değiştirilemezlik. İşlevsel programlama dillerinde çalıştığınız öğelerin çoğu değişmez. Bu özyinelemeyi kolaylaştırır, çünkü zaman içinde nesnelerin durumu ile ilgili endişelenmenize gerek kalmaz. Örneğin altınızdan bir değer değiştirilemez. Ayrıca, birçok dil saf işlevleri algılamak için tasarlanmıştır . Saf fonksiyonların yan etkisi olmadığı için, derleyici, fonksiyonların hangi sırada çalıştığı ve diğer optimizasyonlar konusunda daha fazla özgürlüğe sahiptir.

Bunların hiçbiri diğerlerine karşı işlevsel dillere özgü değildir, bu yüzden daha iyi değiller çünkü işlevseldirler. Ancak fonksiyonel olduklarından, verilen tasarım kararları bu özelliklere yönelecektir, çünkü işlevsel olarak programlama yaparken daha yararlıdır (ve alt kısımları daha az problemlidir).


1
Re: 1. Erken dönüşlerin kuyruk aramaları ile ilgisi yoktur. Bir kuyruk çağrısı ile erken dönebilir ve "geç" geri dönüşün ayrıca bir kuyruk çağrısı özelliğine sahip olmasını sağlayabilirsiniz ve özyinelemeli çağrı kuyruğu konumunda olmayan tek bir basit ifadeye sahip olabilirsiniz (bkz. OP'nin faktör tanımı).

@delnan: Teşekkürler; daha erken ve o şeyi okuduğumdan bu yana epey zaman geçti.
Telastyn

1

Haskell ve diğer fonksiyonel diller genellikle tembel değerlendirme kullanır. Bu özellik, bitmeyen özyinelemeli işlevler yazmanıza olanak sağlar.

Özyinelemenin bittiği bir temel durum tanımlamaksızın özyinelemeli bir işlev yazarsanız, bu işleve sonsuz çağrılar ve yığın akışıyla bitirdiniz.

Haskell ayrıca özyinelemeli fonksiyon çağrısı optimizasyonlarını da destekler. Java'da her bir işlev çağrısı yığılır ve ek yüke neden olur.

Yani evet, işlevsel diller özyinelemeyi diğerlerinden daha iyi ele alır.


5
Haskell çok katı olmayan diller arasındadır - tüm ML ailesi ( tembellik katan bazı araştırma alanları dışında ), tüm popüler Lisps, Erlang, vb. Ayrıca, ikinci paragraf kapalı görünüyor - doğru ilk paragrafta devlet olarak, tembellik etmez , sonsuz bir yineleme (Haskell başlangıcı gayet kullanışlı sahiptir verir forever a = a >> forever aörneğin).

@deinan: Bildiğim kadarıyla SML / NJ de tembel bir değerlendirme sunuyor, ancak SML'ye bir ektir. Ayrıca birkaç tembel işlevsel dilden ikisine de isim vermek istedim: Miranda ve Clean.
Giorgio

1

Bildiğim tek teknik neden, bazı fonksiyonel dillerin (ve hatırladığım bazı zorunlu dillerin), her bir özyinelemeli çağrı ile (örneğin, özyinelemeli çağrı ile yığının boyutunu artırmamasını sağlayan) özyinelemeli bir yönteme sahip olması durumunda, kuyruk çağrısı optimizasyonu denilen şeye sahip olmasıdır . az ya da çok, yığıntaki geçerli çağrıyı değiştirir).

Unutmayın, bu optimizasyon herhangi bir özyinelemeli çağrıda işe yaramaz , yalnızca kuyruk çağrısı özyinelemeli yöntemler (yani özyinelemeli çağrı sırasında durumu korumayan yöntemler)


1
(1) Bu tür bir optimizasyon sadece çok özel durumlarda uygulanır - OP'nin örneği onlar değildir ve birçok basit fonksiyonun kuyruk özyinelemeli olması için bazı ekstra özen göstermeleri gerekir. (2) Gerçek kuyruk çağrısı optimizasyonu sadece özyinelemeli fonksiyonları optimize etmekle kalmaz, aynı zamanda hemen bir geri dönüş tarafından takip edilen herhangi bir çağrıdan ek yükü alandan kaldırır .

@delnan: (1) Evet, çok doğru. Bu cevabın 'orijinal taslağı' nda, şunu söylemiş oldum :( (2) Evet, ama soru bağlamında, bahsetmek istemeyeceğini düşündüm.
Steven Evers

Evet, (2) sadece faydalı bir eklentidir (devam eden stil için vazgeçilmez olsa da), cevaplara gerek yok.

1

Çöp Toplama İşlemine Hızlı Bakmak İstersiniz, Ancak Bir Yığın Daha Hızlı , C programcılarının derlenmiş C'deki yığın çerçeveleri için "yığın" olarak düşüneceklerini kullanma hakkında bir makale . Kesin bir cevap değil, ancak özyinelemeyle ilgili bazı konuları anlamanıza yardımcı olabilir.

Alef programlama dili Bell Labs Planı 9 ile birlikte gelirdi, bir "haline" beyanı (Bkz bölüm içinde 6.6.4 vardı bu referans ). Bir tür açık kuyruk çağırma özyinelemesi optimizasyonu. "Ama çağrı yığınını kullanıyor!" özyinelemeye karşı tartışma potansiyel olarak ortadan kaldırılabilir.


0

TL; DR: Evet,
tekrarlıyorlar Recursion, fonksiyonel programlamada önemli bir araçtır ve bu nedenle bu çağrıları optimize etmek için birçok çalışma yapılmıştır. Örneğin, R5RS (teknik özelliklerde!), Tüm uygulamaların, programcı yığının taşması konusunda endişelenmeden sınırsız özyineleme çağrılarını gerçekleştirmesini gerektirir. Karşılaştırma için, varsayılan olarak C derleyicisi açık bir kuyruk çağrısı optimizasyonu bile yapmaz (bağlantılı listenin özyinelemeli bir tersini deneyin) ve bazı çağrılardan sonra, program sonlandırılır (kullanıyorsanız, derleyici en iyi duruma getirecektir - O2).

Tabii ki, fibüstel olan ünlü örnek gibi, korkunç bir şekilde yazılmış programlarda , derleyicinin sihrini yapma seçeneği yoktur. Bu nedenle, derleyici çabalarının optimizasyondaki çabalarını engellememeye özen gösterilmelidir.

EDIT: Fib örneğiyle, aşağıdakileri kastediyorum:

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)

0

İşlevsel diller iki özel özyinelemede daha iyidir: kuyruk özyinelemesi ve sonsuz özyineleme. Bunlar, factorialörneğin , özyinelemelerdeki diğer diller kadar kötü, örneğin sizin gibi.

Bu, her iki paradigmada da düzenli özyinelemeyle iyi çalışan algoritmalar olmadığını söylemek değildir. Örneğin, bir derinlik birinci ağaç araması gibi, yığın benzeri bir veri yapısı gerektiren herhangi bir şey, yinelemeyle uygulamak için en basittir.

İşlevsel programlama ile özyineleme daha sık ortaya çıkmaktadır, ancak özellikle yeni başlayanlar veya yeni başlayanlar için öğreticiler tarafından da kullanılmamaktadır, çünkü belki de işlevsel programlamaya yeni başlayanlar zorunlu programlamaya başlamadan önce özyinelemeyi kullanmıştır. Liste kavrayışları, üst düzey işlevler ve koleksiyonlardaki diğer işlemler gibi, genellikle kavramsal olarak, stil, öz, verimlilik ve optimize etme kabiliyetine daha uygun olan diğer işlevsel programlama yapıları vardır.

Örneğin, Delnan'ın önerisi factorial n = product [1..n]yalnızca daha özlü ve okunması kolay değil, aynı zamanda oldukça paralelleştirilebilir. Bir dili kullanmak için de geçerlidir foldveya reduceeğer diliniz productzaten yerleşik bir yapıya sahip değilse , özyineleme, bu sorun için son çaredir. Derslerde özyinelemeli bir şekilde çözüldüğünü görmenin temel nedeni, en iyi uygulamaların bir örneği olarak değil, daha iyi çözümlere ulaşmadan önce bir başlangıç ​​noktasıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.