Referans Şeffaflığını Kırma Yan Etkileri


11

Scala'daki Fonksiyonel Programlama , bir yan etkinin referans şeffaflığını kırma üzerindeki etkisini açıklar:

referans şeffaflığın bir miktar ihlali anlamına gelen yan etki.

Bir programı değerlendirmek için “ikame modelinin” kullanımını tartışan SICP'nin bir bölümünü okudum .

Ben şöyle kabaca referans saydamlığını (RT) ile ikame modeli anlamak, onun en basit bölüme bir işlev de-oluşturabilirsiniz. İfade RT ise, ifadeyi kaldırabilir ve her zaman aynı sonucu alabilirsiniz.

Bununla birlikte, yukarıdaki alıntıda belirtildiği gibi, yan etkilerin kullanılması ikame modelini bozabilir / bozacaktır.

Misal:

val x = foo(50) + bar(10)

Yan etkileri yoksa foove bar yoksa , her iki işlevi yürütmek her zaman aynı sonucu döndürür x. Ancak, yan etkileri varsa, ikame modeline bir anahtarı bozan / atan bir değişkeni değiştireceklerdir.

Bu açıklamada kendimi rahat hissediyorum ama tam anlamıyla anlayamıyorum.

Lütfen beni düzeltin ve RT'yi bozan yan etkilerle ilgili olarak, ikame modeli üzerindeki etkileri de tartışarak delikleri doldurun.

Yanıtlar:


20

Referans şeffaflığı için bir tanımla başlayalım :

Bir ifadenin, bir programın davranışını değiştirmeden değeriyle değiştirilebiliyorsa referans olarak saydam olduğu söylenir (diğer bir deyişle, aynı girdide aynı efektlere ve çıktıya sahip bir program verir).

Bunun anlamı, (örneğin) 2 + 5'i programın herhangi bir bölümünde 7 ile değiştirebileceğiniz ve programın hala çalışması gerektiğidir. Bu sürece ikame denir . Programın başka bir bölümünü etkilemeden 2 + 5'in 7 ile değiştirilebilmesi durumunda ikame geçerlidir .

Diyelim ki Bazişlevleri olan Foove Bariçinde bir sınıf var. Basitlik için, sadece şunu söyleyeceğiz Foove Barher ikisi de aktarılan değeri döndürecektir. Yani Foo(2) + Bar(5) == 7, beklediğiniz gibi. Referans Şeffaflığı Foo(2) + Bar(5), ifadeyi 7programınızın herhangi bir yerinde ifadeyle değiştirebileceğinizi garanti eder ve program yine de aynı şekilde çalışır.

Ancak Foo, iletilen değeri döndürdüyse, ancak Bariletilen değeri ve verilen son değeriFoo döndürdüyse ne olur ? Sınıf Fooiçindeki yerel bir değişkende değerini saklarsanız bunu yapmak yeterince kolaydır Baz. Bu yerel değişkenin başlangıç ​​değeri 0 ise, ifade ilk çağırdığınızda Foo(2) + Bar(5)beklenen değeri döndürür 7, ancak 9ikinci çağırdığınızda döndürür .

Bu, referans şeffaflığını iki şekilde ihlal etmektedir. İlk olarak, her çağrıldığında aynı ifadeyi döndürmek için Çubuğa güvenilemez. İkincisi, bir yan etki, yani Foo'nun çağrılması Bar'ın dönüş değerini etkiler. Artık Foo(2) + Bar(5)7'ye eşit olacağını garanti edemeyeceğiniz için , artık ikame edemezsiniz.

Başvuruda Şeffaflığın pratikte anlamı budur; referans olarak saydam bir işlev, programın başka bir yerinde başka bir kodu etkilemeden bir değer kabul eder ve karşılık gelen bir değer döndürür ve her zaman aynı girdi verildiğinde aynı çıktıyı döndürür.


5
Yani kırma RTkullanmasını devre dışı bırakır sizi substitution model.ile büyük bir sorun değil kullanmak mümkün olan substitution modelbir program hakkında nedenden için kullanarak gücüdür?
Kevin Meredith

Bu kesinlikle doğru.
Robert Harvey

1
+1 harika net ve anlaşılır cevap. Teşekkür ederim.
Racheet

2
Ayrıca, bu işlevler saydam veya "saf" ise, gerçekte çalıştıkları sıra önemli değilse, önce foo () veya bar () çalışıp çalışmadığını umursamayız ve bazı durumlarda gerekmediklerini asla değerlendiremezler
Zachary K

1
RT'nin yine bir başka avantajı pahalı referans olarak şeffaf ifadelerin önbelleğe alınabilmesidir (çünkü bunları bir veya iki kez değerlendirmek aynı sonucu vermelidir).
dcastro

3

Bir duvar inşa etmeye çalıştığınızı ve size farklı boyutlarda ve şekillerde çeşitli kutular verildiğini düşünün. Duvardaki belirli bir L şekilli deliği doldurmanız gerekir; L şeklinde bir kutu aramalı mıyım yoksa uygun boyutta iki düz kutu mu değiştirebilir misiniz?

İşlevsel dünyada, cevap her iki çözümün de işe yarayacağıdır. İşlevsel dünyanızı inşa ederken, içeride ne olduğunu görmek için asla kutuları açmanız gerekmez.

Zorunlu dünyasında, her kutuda içerikleri araştırırken olmadan duvar inşa etmek tehlikelidir ve diğer her kutu içeriğine karşılaştırarak:

  • Bazıları güçlü mıknatıslar içerir ve yanlış hizalanırsa diğer manyetik kutuları duvardan iter.
  • Bazıları çok sıcak veya soğuktur ve bitişik alanlara yerleştirilirse kötü tepki verir.

Sanırım daha olası olmayan benzetmelerle zamanınızı boşa harcamamadan duracağım, ama umarım bu konuya değinilir; fonksiyonel tuğlalar gizli sürprizler içermez ve tamamen öngörülebilirdir. Daha büyük olanın yerini almak için her zaman doğru boyut ve şekildeki daha küçük blokları kullanabileceğiniz ve aynı boyut ve şekle sahip iki kutu arasında fark olmadığı için referans şeffaflığınız vardır. Zorunlu tuğlalarla, doğru büyüklükte ve şekilde bir şeye sahip olmak yeterli değildir - tuğlaların nasıl inşa edildiğini bilmek zorundasınız. Referans olarak şeffaf değil.

Saf işlevsel bir dilde, tek yapmanız gereken bir işlevin ne yaptığını bilmek imzasıdır. Tabii ki, ne kadar iyi performans gösterdiğini görmek için içeri bakmak isteyebilirsiniz, ama yok olması görünüme.

Zorunlu bir dilde, içinde hangi sürprizlerin gizlenebileceğini asla bilemezsiniz.


"Saf işlevsel bir dilde, tek yapmanız gereken bir işlevin ne yaptığını bilmek imzasıdır." - Bu genellikle doğru değil. Evet, parametrik polimorfizm varsayımı altında biz türünde bir fonksiyonu olduğu sonucuna varabiliriz (a, b) -> asadece olabilir fstfonksiyon ve türü bir fonksiyonu olduğunu a -> asadece olabilir identityfonksiyonu, ancak mutlaka tip bir fonksiyonu hakkında bir şey söyleyemem (a, a) -> aörneğin.
Jörg W Mittag

2

İkame modelini kabaca anladığım için (referans şeffaflığı (RT) ile), bir işlevi en basit parçalarına ayırabilirsiniz. İfade RT ise, ifadeyi kaldırabilir ve her zaman aynı sonucu alabilirsiniz.

Evet, sezgi oldukça doğru. Daha kesin olmak için birkaç işaret:

Dediğiniz gibi, herhangi bir RT ifadesinin bir single"sonucu" olmalıdır. Yani, factorial(5)programda bir ifade verildiğinde , her zaman aynı "sonucu" vermelidir. Bu nedenle, factorial(5)programda belirli bir değer varsa ve 120 verirse, zamana bakılmaksızın hangi "adım sırası" genişletildiği / hesaplandığından bağımsız olarak daima 120 vermelidir .

Örnek: factorialişlev.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Bu açıklama ile ilgili birkaç husus vardır.

Her şeyden önce, farklı değerlendirme modellerinin (bkz. Uygulanabilir veya normal sıraya bakın) aynı RT ifadesi için farklı "sonuçlar" verebileceğini unutmayın.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

Yukarıdaki kodda firstve secondreferans olarak şeffaftır ve yine de, sondaki ifade, normal düzen ve uygulanabilir düzen altında değerlendirildiğinde farklı "sonuçlar" verir (ikincisinin altında, ifade durmaz).

.... tırnak içinde "sonuç" kullanımına yol açar. Bir ifadenin durması gerekmediğinden, bir değer üretmeyebilir. Yani "sonuç" kullanmak biraz bulanık. Bir RT ifadesinin computationsbir değerlendirme modeli altında her zaman aynı sonucu verdiği söylenebilir .

Üçüncüsü, foo(50)programda farklı yerlerde iki ifadenin farklı ifadeler olarak görünmesi gerekebilir - her biri birbirinden farklı olabilecek kendi sonuçlarını verir. Örneğin, dil dinamik kapsama izin veriyorsa, her iki ifade de, sözcüksel olarak özdeş olsa da, farklıdır. Perl dilinde:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

Dinamik kapsam misleads o kadar kolay bir düşünmek için yapmak çünkü xtek girdidir foogerçekte, o zaman, xve y. Farkı görmenin bir yolu, programı dinamik kapsamı olmayan eşdeğer bir programa dönüştürmektir - yani, parametreleri açıkça geçirerek, tanımlamak yerine , arayanları açıkça foo(x)tanımlar foo(x, y)ve yiletiriz.

Mesele şu ki, her zaman bir functionzihniyet altındayız : bir ifade için belirli bir girdi verildiğinde, buna karşılık gelen bir "sonuç" verilir. Aynı girdiyi verirsek, her zaman aynı "sonucu" beklemeliyiz.

Şimdi, aşağıdaki kod ne olacak?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

Yeniden footanımlamalar olduğundan prosedür RT'yi keser. Yani, ybir noktada tanımladık ve ikincisi de aynı şeyi yeniden tanımladık y. Yukarıdaki perl örneğinde, ys, aynı "y" harf adını paylaşsalar da farklı bağlardır. Burada ys aslında aynı. Bu yüzden atamanın bir meta işlem olduğunu söylüyoruz : aslında programınızın tanımını değiştiriyorsunuz.

Kabaca, insanlar genellikle farkı şu şekilde tasvir eder: yan etkisi olmayan bir ortamda, bir eşlemeniz vardır input -> output. "Zorunlu" bir ortamda, zaman input -> ouputiçinde statedeğişebilen bir bağlamda var .

Şimdi, ifadeleri sadece karşılık gelen değerleri yerine koymak yerine state, bunu gerektiren her işlemde de dönüşümler uygulanmalıdır (ve tabii ki ifadeler statehesaplamaları gerçekleştirmek için buna danışabilir ).

Dolayısıyla, bir yan etki serbest programında, bir ifadeyi hesaplamak için bilmemiz gereken tek şey onun bireysel girdisidir, zorunlu bir programda, her bir hesaplama adımı için girdileri ve tüm durumu bilmemiz gerekir. Akıl yürütme ilk kez büyük bir darbe çekiyor (şimdi, sorunlu bir prosedürde hata ayıklamak için, girdiye ve çekirdek dökümü gerekir). Bazı numaralar ezberleme gibi pratik değildir. Ama aynı zamanda, eşzamanlılık ve paralellik çok daha zorlaşıyor.


1
Memoizasyondan bahsettiğiniz güzel. Bu, dışarıda görünmeyen bir iç durum örneği olarak kullanılabilir: dahili olarak durum ve mutasyon kullansa bile, notu kullanan bir işlev yine de referans olarak saydamdır.
Giorgio
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.