Bu saf bir işlev mi?


117

Çoğu kaynak , saf bir işlevi aşağıdaki iki özelliğe sahip olarak tanımlar:

  1. Dönüş değeri aynı argümanlar için aynıdır.
  2. Değerlendirmesinin yan etkisi yoktur.

Beni ilgilendiren ilk durum bu. Çoğu durumda, yargılamak kolaydır. Aşağıdaki JavaScript işlevlerini göz önünde bulundurun ( bu makalede gösterildiği gibi )

Saf:

const add = (x, y) => x + y;

add(2, 4); // 6

Kirli:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

2. işlevin sonraki çağrılar için farklı çıkışlar vereceğini ve böylece ilk koşulu ihlal edeceğini görmek kolaydır. Ve dolayısıyla, saf değil.

Aldığım bu kısım.


Şimdi, sorum için, belirli bir tutarı avroya dönüştüren bu işlevi düşünün:

(DÜZENLE - constİlk satırda kullanma . letDaha önce yanlışlıkla kullanılmış.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Döviz kurunu bir db'den aldığımızı ve her gün değiştiğini varsayalım.

Şimdi, bu işlevi çağırmak olursa olsun kaç kez bugün , bana girişi için aynı çıktıyı verecektir 100. Ancak, yarın bana farklı bir çıktı verebilir. Bunun ilk koşulu ihlal edip etmediğinden emin değilim.

IOW, işlevin kendisi girişi değiştirmek için herhangi bir mantık içermez, ancak gelecekte değişebilecek harici bir sabite dayanır. Bu durumda, her gün değişeceği kesinlikle kesindir. Diğer durumlarda, olabilir; olmayabilir.

Bu tür işlevlere saf işlevler diyebilir miyiz? Cevabınız HAYIR ise, nasıl yanıt verebiliriz?


6
JS gibi dinamik bir dilin function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
saflığı

29
Saflık, program çağrısını değiştirmeden işlev çağrısını kod düzeyinde sonuç değeriyle değiştirebileceğiniz anlamına gelir.
bob

1
Yan etkiyi neyin oluşturduğuna ve daha teorik terminolojiye ilişkin biraz daha ileriye gitmek için, bkz. Cs.stackexchange.com/questions/116377/…
Gilles 'SO- kötü'

3
Bugün, işlev (x) => {return x * 0.9;}. Yarın, belki de saf olacak farklı bir işleve sahip olacaksınız (x) => {return x * 0.89;}. Her çalıştırdığınızda yeni bir işlev (x) => {return x * exchangeRate;}oluşturduğuna ve bu işlevin saf olduğuna dikkat edin, çünkü değişemez. exchangeRate
user253751

2
Bu saf olmayan bir işlevdir, eğer saf yapmak istiyorsanız, const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; saf bir işlev için kullanabilirsiniz , Its return value is the same for the same arguments.her zaman, 1 saniye, 1 on yıl beklemelisiniz .. sonra ne olursa olsun
Vikash Tiwari

Yanıtlar:


133

dollarToEuroBireyin dönüş değeri bir argüman değil dışarıdan bir değişkene bağlıdır; bu nedenle, işlev saf değildir.

Cevabında HAYIR, o zaman işlevi saf olarak nasıl yeniden düzenleyebiliriz?

Bir seçenek geçmek exchangeRate. Bu şekilde, her argüman olduğunda (something, somethingElse), çıktının şu şekilde olacağı garanti edilir something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Fonksiyonel programlama için kaçınmanız gerektiğini unutmayın let- constyeniden atamayı önlemek için daima kullanın .


6
Serbest değişkenleri sahip değil değil : bir işlev saf olmamız için bir gereklilik const add = x => y => x + y; const one = add(42);Burada hem addve onesaf fonksiyondur.
zerkms

7
const foo = 42; const add42 = x => x + foo;<- Bu, yine serbest değişkenleri kullanan başka bir saf işlevdir.
zerkms

8
@zerkms - Bu soruya cevabınızı görmeye çok hevesli olurum (sadece belirli terminoloji kullanmak için CertainPerformance'ın geri sarılsa bile). Çoğaltacağını düşünmüyorum ve özellikle alıntı yapıldığında aydınlatıcı olurdu (ideal olarak yukarıdaki Wikipedia makalesinden daha iyi kaynaklarla, ancak elde ettiğimiz her şey buysa, hala bir kazanç). (Bu yorumu bir çeşit olumsuz ışıkta okumak kolay olurdu. Bana gerçek olduğuma güven, bence böyle bir cevap harika olur ve okumak ister.)
TJ Crowder

17
Hem sen hem de @zerkms'in yanlış olduğunu düşünüyorum. Bunu düşünüyor dollarToEuroserbest değişkene bağlıdır çünkü cevap örnekteki fonksiyon katışıktır exchangeRate. Bu çok saçma. Zerkms'in işaret ettiği gibi, bir fonksiyonun saflığının, serbest değişkenlere sahip olup olmadığı ile ilgisi yoktur. Bununla birlikte, zerkms de yanlıştır, çünkü dollarToEuroişlevin saf olmadığına inanır çünkü exchangeRatehangisinin veritabanından geldiğine bağlıdır . Bunun saf dışı olduğunu söylüyor çünkü "bu geçici olarak ES'ye bağlı."
Aadit M Shah

9
(devam) Yine, bu saçma çünkü serbest bir değişken dollarToEuroolduğu için saf olmadığını gösteriyor exchangeRate. O takdirde düşündürmektedir exchangeRatebir argüman olsaydı serbest değişken, yani değildi, daha sonra dollarToEurosaf olurdu. Bu nedenle, bunun dollarToEuro(100)saf olmayan ama dollarToEuro(100, exchangeRate)saf olduğunu gösterir. Bu açıkça saçma çünkü her iki durumda exchangeRateda bir veritabanından hangisine bağlı olduğunuza bağlısınız . Tek fark, fonksiyon exchangeRateiçinde serbest bir değişken olup olmadığıdır dollarToEuro.
Aadit M Shah

76

Teknik olarak, bir bilgisayarda yürüttüğünüz herhangi bir program saf değildir, çünkü sonunda “bu değeri içine taşı eax” ve “bu değeri içeriğine ekleyin” gibi saf olmayan talimatlara derler eax. Bu çok yardımcı değil.

Bunun yerine, kara kutuları kullanarak saflığı düşünüyoruz . Bazı kodlar, aynı girdiler verildiğinde her zaman aynı çıktıları üretirse, o zaman saf kabul edilir. Bu tanıma göre, dahili olarak saf olmayan not tablosu kullanmasına rağmen aşağıdaki işlev de saftır.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

İçselliği umursamıyoruz çünkü saflığı kontrol etmek için bir kara kutu metodolojisi kullanıyoruz. Benzer şekilde, tüm kodların nihayetinde kirli makine talimatlarına dönüştürülmesine aldırmıyoruz çünkü bir kara kutu metodolojisi kullanarak saflığı düşünüyoruz. İç kısımlar önemli değil.

Şimdi, aşağıdaki işlevi göz önünde bulundurun.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

greetFonksiyon saf mı yoksa saf mı? Kara kutu metodolojimize göre, aynı girişi verirsek (örn. World) Her zaman aynı çıkışı ekrana yazdırır (yani Hello World!). Bu anlamda saf değil mi? Hayır değil. Saf olmasının nedeni, ekrana bir şey yazdırmayı bir yan etki olarak görmemizdir. Kara kutumuz yan etkiler üretiyorsa, o zaman saf değildir.

Yan etki nedir? Referans şeffaflığı kavramının yararlı olduğu yer burasıdır . Bir işlev referans olarak saydamsa, o işlevin uygulamalarını her zaman sonuçlarıyla değiştirebiliriz. Bunun, işlev satır içi ile aynı olmadığını unutmayın .

Satır içi işlevde, programın anlamını değiştirmeden bir işlevin uygulamalarını işlevin gövdesi ile değiştiririz. Bununla birlikte, referans olarak saydam bir işlev, programın anlamını değiştirmeden her zaman dönüş değeri ile değiştirilebilir. Aşağıdaki örneği ele alalım.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Burada, tanımını vurguladık greetve programın anlambilimini değiştirmedi.

Şimdi, aşağıdaki programı düşünün.

undefined;
undefined;

Burada, greetişlevin uygulamalarını geri dönüş değerleriyle değiştirdik ve programın anlambilimini değiştirdi. Artık ekrana selam basmıyoruz. Yazdırmanın bir yan etki olarak kabul edilmesinin nedeni budur ve bu nedenle greetişlev saf değildir. Referans olarak şeffaf değil.

Şimdi başka bir örnek ele alalım. Aşağıdaki programı düşünün.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Açıkçası, mainişlev saf değildir. Ancak, timeDiffişlev saf mı yoksa saf değil mi? serverTimeSaf olmayan bir ağ çağrısından hangisinin geldiğine bağlı olmasına rağmen , aynı girdiler için aynı çıkışları döndürdüğü ve herhangi bir yan etkisi olmadığı için yine de referans olarak şeffaftır.

zerkms muhtemelen bu noktada benimle aynı fikirde değil. Onun cevabında , dollarToEuroaşağıdaki örnekteki işlevin saf olmadığını, çünkü “geçişken IO'ya bağlıdır” dedi.

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Onunla aynı fikirde olmam gerekiyor çünkü exchangeRatebir veritabanından gelenlerin alakasız olması. Bu bir iç detaydır ve bir işlevin saflığını belirlemek için kara kutu metodolojimiz iç detayları önemsemez.

Haskell gibi tamamen işlevsel dillerde, keyfi G / Ç efektleri uygulamak için bir kaçış kapağı var. Deniyor unsafePerformIOve doğru şekilde kullanmak istemiyorsanız adından da anlaşılacağı gibi bu referans şeffaflık bozabilir çünkü o zaman güvenli değil. Ancak, ne yaptığınızı biliyorsanız, o zaman kullanmak tamamen güvenlidir.

Genellikle programın başlangıcına yakın yapılandırma dosyalarından veri yüklemek için kullanılır. Yapılandırma dosyalarından veri yüklemek, saf olmayan bir IO işlemidir. Ancak, verileri her fonksiyona girdi olarak iletmekle yükümlü olmak istemiyoruz. Dolayısıyla, eğer kullanırsak unsafePerformIO, verileri en üst seviyede yükleyebiliriz ve tüm saf fonksiyonlarımız değişmez global yapılandırma verilerine bağlı olabilir.

Bir fonksiyonun bir yapılandırma dosyasından, veritabanından veya ağ çağrısından yüklenen bazı verilere bağlı olması, işlevin saf olmadığı anlamına gelmediğini unutmayın.

Bununla birlikte, farklı anlambilime sahip orijinal örneğinizi düşünelim.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Burada, exchangeRateolarak tanımlanmadığı için const, program çalışırken değiştirileceğini varsayıyorum . Eğer durum buysa dollarToEuro, kesinlikle saf olmayan bir işlevdir çünkü exchangeRatedeğiştirildiği zaman referans şeffaflığını kıracaktır.

Ancak, exchangeRatedeğişken değiştirilmezse ve gelecekte hiçbir zaman değiştirilmezse (yani, sabit bir değerse), o zaman tanımlanmış olmasına rağmen let, referans şeffaflığını bozmaz. Bu durumda, dollarToEurogerçekten saf bir işlevdir.

exchangeRateProgramı her çalıştırdığınızda ve değerinin değişebileceğini ve referans şeffaflığını bozmayacağını unutmayın. Referans şeffaflığını yalnızca program çalışırken değişirse keser.

Örneğin, timeDifförneğimi birden çok kez çalıştırırsanız, farklı değerler serverTimeve dolayısıyla farklı sonuçlar elde edersiniz . Ancak, serverTimeprogram çalışırken asla değeri değişmediği için timeDiffişlev saftır.


3
Bu çok bilgilendiriciydi. Teşekkürler. Ve benim örneğimde kullanmak constistedim.
Kardan Adam

3
Kullanmak ortalama yaptıysak consto zaman dollarToEuroişlev gerçekten saftır. Değerinin exchangeRatedeğişmesinin tek yolu , programı tekrar çalıştırmanızdır. Bu durumda, eski süreç ve yeni süreç farklıdır. Dolayısıyla, referans şeffaflığını bozmaz. Farklı argümanlarla iki kez bir fonksiyon çağırmak gibidir. Bağımsız değişkenler farklı olabilir, ancak işlev içinde bağımsız değişkenlerin değeri sabit kalır.
Aadit M Shah

3
Bu görelilikle ilgili küçük bir teori gibi geliyor: sabitler sadece göreceli olarak sabittir, kesinlikle değil, yani çalışan sürece göre. Açıkçası buradaki tek doğru cevap. +1.
bob

5
"Bu değeri eax'a taşıyın ve" bu değeri eax'ın içeriğine ekleyin "gibi talimatlara göre derlendiğinden emin değilim . eaxYüklü veya açık bir şekilde temizlenirse, kod ne olursa olsun belirleyici kalır başka neler oluyor ve bu yüzden
saf.Aksi

3
@Bergi: Aslında, değişmez değerleri olan saf bir dilde, kimlik önemsizdir. Aynı değere değerlendirmek iki referans aynı nesneye veya farklı nesnelere iki referans olsun sadece gözlemlenebilir mutasyona referanslar biri boyunca nesneye ve diğer referans yoluyla alınan zaman değeri de değişir olup gözlemleyerek. Mutasyon olmadan kimlik önemsiz hale gelir. (Rich Hickey'in dediği gibi: Kimlik Zamanla bir dizi
Devlettir

23

Bir me-puristin cevabı (burada "ben" tam anlamıyla ben, çünkü bu sorunun tek bir resmi "doğru" cevabı olmadığını düşünüyorum ):

JS gibi temel bir dilde, yama taban türlerini maymunlamak için çok fazla seçeneğe sahip veya Object.prototype.valueOfbir işlevin sadece ona bakarak saf olup olmadığını söylemek imkansız gibi özellikleri kullanarak özel türler oluşturuyor , çünkü arayanların istedikleri veya istemedikleri konusunda yan etkiler üretmek.

Bir demo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Bana pragmatistin cevabı:

Gönderen wikipedia dan çok tanım

Bilgisayar programlamasında saf işlev, aşağıdaki özelliklere sahip bir işlevdir:

  1. Dönüş değeri aynı bağımsız değişkenler için aynıdır (yerel statik değişkenler, yerel olmayan değişkenler, değiştirilebilir referans bağımsız değişkenleri veya G / Ç aygıtlarından gelen giriş akışları ile değişiklik yok).
  2. Değerlendirmesinin yan etkisi yoktur (lokal statik değişkenlerin, lokal olmayan değişkenlerin, değişken referans argümanlarının veya G / Ç akışlarının mutasyonu yoktur).

Başka bir deyişle, bir işlevin nasıl davrandığı değil, sadece bir işlevin nasıl davrandığı önemlidir. Ve belirli bir işlev bu 2 özelliği taşıdığı sürece - tam olarak nasıl uygulandığından bağımsız olarak saftır.

Şimdi fonksiyonunuza:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Zorunlu değil çünkü gereksinimi 2 nitelendirmiyor: IO'ya geçişli olarak bağlı.

Yukarıdaki ifadenin yanlış olduğunu kabul ediyorum, ayrıntılar için diğer cevaba bakın: https://stackoverflow.com/a/58749249/251311

Diğer ilgili kaynaklar:


4
@TJCrowder mecevap veren zerkms olarak.
zerkms

2
Evet, Javascript ile her şey güven, garanti değil
bob

4
@bob ... ya da engelleme çağrısı.
zerkms

1
@zerkms - Teşekkürler. Sadece% 100 eminim, sizin add42ve benim arasındaki temel fark addXtamamen benim xdeğiştirilebileceğidir ve ftdeğiştirilemez (ve böylece add42dönüş değeri esas alınmaz ft)?
TJ Crowder

5
Örneğinizdeki dollarToEuroişlevin saf olmadığını kabul etmiyorum. Cevabımda neden katılmıyorum diye açıkladım. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

Diğer cevapların söylediği gibi, uygulama şekliniz dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

gerçekten saftır, çünkü program çalışırken döviz kuru güncellenmez. Bununla birlikte, kavramsal olarak, dollarToEuroen güncel döviz kuru ne olursa olsun, saf olmayan bir işlev gibi görünmektedir. Bu tutarsızlığı açıklamanın en basit yolu, uygulayamadığınız dollarToEuroama değil dollarToEuroAtInstantOfProgramStart.

Buradaki anahtar, bir para birimi dönüşümünü hesaplamak için gereken birkaç parametrenin olması ve generalin gerçekten saf bir versiyonunun dollarToEurohepsini tedarik etmesidir. En doğrudan parametreler dönüştürülecek USD miktarı ve döviz kurudur. Bununla birlikte, döviz kurunuzu yayınlanan bilgilerden almak istediğiniz için, şimdi sağlamak için üç parametreniz var:

  • Takas edilecek para miktarı
  • Döviz kurlarına danışmak için tarihi bir otorite
  • İşlemin gerçekleştiği tarih (tarihsel otoriteyi endekslemek için)

Buradaki tarihsel otorite veritabanınızdır ve veritabanının güvenliğinin ihlal edilmediğini varsayarsak, belirli bir günde döviz kuru için her zaman aynı sonucu döndürür. Bu nedenle, bu üç parametrenin kombinasyonu ile, genelin tamamen saf, kendi kendine yeterli bir versiyonunu yazabilirsiniz dollarToEuro, bu böyle bir şeye benzeyebilir:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Uygulamanız, işlevin oluşturulduğu anda hem tarihi otorite hem de işlem tarihi için sabit değerleri yakalar - tarihsel otorite veritabanınızdır ve yakalanan tarih programı başlattığınız tarihtir - kalan tek şey dolar tutarıdır arayanın sağladığı. Bunun saf olmayan sürümü dollarToEuroher zaman en güncel değeri alır, aslında date parametresini dolaylı olarak alır ve işlevin çağrıldığı anı ayarlar, bu basit değildir, çünkü işlevi asla aynı parametrelerle iki kez çağıramazsınız.

dollarToEuroYine de en güncel değeri alabilen saf bir sürümüne sahip olmak istiyorsanız, yine de tarihsel otoriteyi bağlayabilirsiniz, ancak date parametresini bağlı bırakıp sona eren bir argüman olarak tarihi isteyebilirsiniz böyle bir şeyle:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@ Hoş geldiniz! Daha fazla kod örneği eklemek için cevabı biraz güncelledim.
TheHansinator

8

JS'nin belirli detaylarından ve resmi tanımların soyutlamasından biraz uzaklaşmak ve belirli optimizasyonları etkinleştirmek için hangi koşulların tutulması gerektiğinden bahsetmek istiyorum. Bu genellikle kod yazarken önem verdiğimiz ana şeydir (doğruluğunu kanıtlamasına rağmen). İşlevsel programlama, ne en son modalara rehberlik eder ne de manastırın kendini reddetme yeminidir. Sorunları çözmek için bir araçtır.

Böyle bir kodunuz olduğunda:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Eğer exchangeRateiki çağrı arasında değiştirilebilir asla üzere dollarToEuro(100)birinciye çağrısının sonucunu not-ize için, bu mümkün dollarToEuro(100)uzakta ve optimize ikinci çağrı. Sonuç aynı olacaktır, bu yüzden önceki değeri hatırlayabiliriz.

exchangeRateHerhangi bir fonksiyonu olduğunu görünüyor o kadar çağırmadan önce, bir kez ayarlanır ve modifiye asla olabilir. Daha az kısıtlayıcı olarak, exchangeRatebelirli bir işlev veya kod bloğu için bir kez arama yapan ve bu kapsamda aynı döviz kurunu tutarlı bir şekilde kullanan bir kodunuz olabilir. Ya da, yalnızca bu iş parçacığı veritabanını değiştirebilirse, döviz kurunu güncellemediyseniz, başka hiç kimsenin bunu sizin üzerinde değiştirmediğini varsayabilirsiniz.

Eğer fetchFromDatabase()kendisi bir sabite değerlendiren saf fonksiyondur ve exchangeRatedeğişmez, bu sabiti hesaplama yoluyla tüm yol kat olabilir. Durumun böyle olduğunu bilen bir derleyici, yorumda yaptığınız aynı çıkarımda dollarToEuro(100)bulunarak 90.0 değerini değerlendirebilir ve tüm ifadeyi 90.0 sabitiyle değiştirebilir.

Bununla birlikte, fetchFromDatabase()bir yan etki olarak kabul edilen I / O gerçekleştirmezse, adı En Küçük Şaşkınlık İlkesini ihlal eder.


8

Bu işlev saf değildir, neredeyse kesinlikle değişecek olan bir dış değişkene dayanır.

Bu nedenle işlev, ilk yaptığınız noktayı başarısız olur, aynı argümanlar için aynı değeri döndürmez.

Bu işlevi "saf" yapmak exchangeRateiçin argüman olarak aktarın.

Bu daha sonra her iki koşulu da karşılar.

  1. Aynı değer ve döviz kurundan geçerken her zaman aynı değeri döndürür.
  2. Ayrıca hiçbir yan etkisi yoktur.

Örnek kod:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
"Bu neredeyse kesinlikle değişecek" --- değil, öyle const.
zerkms

7

Diğerlerinin referans şeffaflığı hakkında yaptığı noktaları genişletmek için: saflığı, basitçe fonksiyon çağrılarının referans şeffaflığı olarak tanımlayabiliriz (yani fonksiyona yapılan her çağrı, programın anlamını değiştirmeden dönüş değeri ile değiştirilebilir).

Verdiğiniz iki özellik her ikisi de referans saydamlığının sonuçlarıdır . Örneğin, aşağıdaki işlev f1saf değildir, çünkü her seferinde aynı sonucu vermez (1 olarak numaralandırdığınız özellik):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Her seferinde aynı sonucu almak neden önemlidir? Çünkü farklı sonuçlar elde etmek, bir işlev çağrısının bir değerden farklı semantiğe sahip olmasının ve dolayısıyla referans şeffaflığını kırmanın bir yoludur.

Diyelim ki kodu yazıyoruz, f1("hello", "world")çalıştırıyoruz ve dönüş değerini alıyoruz "hello". Her çağrıyı bul / değiştir f1("hello", "world")ve yerine "hello"koyarsak, programın anlamını değiştirmiş oluruz (tüm çağrılar şimdi yerine geçecektir "hello", ancak başlangıçta bunların yaklaşık yarısı değerlendirilecektir "world"). Bu nedenle, çağrılar f1referans olarak şeffaf değildir, dolayısıyla f1saf değildir.

Bir işlev çağrısının bir değerden farklı anlambilimine sahip olabilmesinin başka bir yolu da deyimler çalıştırmaktır. Örneğin:

function f2(x) {
  console.log("foo");
  return x;
}

Dönüş değeri f2("bar")her zaman olacaktır "bar", ancak değerin anlambilimi "bar"çağrıdan farklıdır, f2("bar")çünkü ikincisi de konsola oturum açar. Birini diğeriyle değiştirmek, programın anlambilimini değiştirir, bu nedenle referans olarak şeffaf değildir ve bu nedenle f2saf değildir.

Senin olsun dollarToEurofonksiyonu referentially şeffaf (ve dolayısıyla saf) iki şeye bağlıdır:

  • Referans olarak şeffaf olarak gördüğümüz şeyin 'kapsamı'
  • Bu exchangeRate'kapsam' içinde hiç değişip değişmeyecek

Kullanılacak "en iyi" kapsam yoktur; normalde programın tek bir çalışmasını veya projenin ömrünü düşünürdük. Bir benzetme olarak, her işlevin dönüş değerlerinin önbelleğe alındığını düşünün (@ aadit-m-shah tarafından verilen örnekte not tablosu gibi): eski değerlerin semantik?

Eğer exchangeRatekullandığınız varo zaman her çağrısı arasında değişebilir dollarToEuro; her arama arasında önbelleğe alınmış sonuçları temizlememiz gerekir, bu nedenle konuşmak için herhangi bir referans şeffaflığı olmazdı.

Kullanarak constprograma bir run 'kapsamını' açılıyorsanız: Bu önbellek dönüş değerlerine güvenli olacağını dollarToEuroprogramı bitinceye kadar. İşlev çağrılarını dönüş değerleriyle değiştirmek için bir makro (Lisp gibi bir dilde) kullanmayı hayal edebiliriz. Bu saflık miktarı, yapılandırma değerleri, komut satırı seçenekleri veya benzersiz kimlikler için ortaktır. Programa bir vadede düşünmeye kendimizi sınırlamak edersek saflık faydalarının en iyi şekilde, ama biz dikkatli olmak zorunda karşısında (örneğin daha sonra başka bir vadede yüklemeden bir dosyaya veri kaydetme) çalışır. Bu tür işlevleri soyut anlamda "saf" olarak adlandırmazdım (örneğin, bir sözlük tanımı yazıyor olsaydım), ancak onları bağlam içinde saf olarak ele almakta sorun yaşamadım .

Projenin ömrünü 'kapsamımız' olarak ele alırsak, o zaman "en referans olarak şeffaf" ve dolayısıyla soyut anlamda bile "en saf" oluruz. Varsayımsal önbelleğimizi asla temizlememiz gerekmeyecekti. Bu "önbelleğe alma" işlevini, çağrıları dönüş değerleriyle değiştirmek için doğrudan diskteki kaynak kodunu yeniden yazarak da yapabiliriz. Bu olur hatta çalışma genelinde projeler mesela biz (o DB ise) herkes bir işlev çağrısı ve bakabilirsiniz bir online fonksiyonların veritabanı ve dönüş değerlerini, hayal diğer tarafında birileri tarafından sağlanan dönüş değeri kullanabilirsiniz yıllar önce farklı bir projede özdeş bir işlevi kullanan dünya.


4

Yazıldığı gibi, saf bir işlevdir. Hiçbir yan etkisi yoktur. Fonksiyonun bir resmi parametresi vardır, ancak iki girişi vardır ve her iki giriş için her zaman aynı değeri verir.


2

Bu tür işlevlere saf işlevler diyebilir miyiz? Cevabınız HAYIR ise, nasıl yanıt verebiliriz?

Doğru şekilde belirttiğiniz gibi, "yarın bana farklı bir çıktı verebilir" . Böyle bir durumda, cevap kocaman bir "hayır" olur . Bu, özellikle hedeflediğiniz davranışınız dollarToEurodoğru olarak yorumlandıysa böyledir :

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Bununla birlikte, saf olarak kabul edileceği farklı bir yorum vardır:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro doğrudan yukarıda saf.


Yazılım mühendisliği perspektifinden bakıldığında, dollarToEuroişleve bağımlılığı beyan etmek esastır fetchFromDatabase. Bu nedenle, dollarToEuroaşağıdaki gibi tanımı yeniden düzenleyin :

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Bu sonuçla, fetchFromDatabasetatmin edici bir şekilde işlev gören öncül göz önüne alındığında, fetchFromDatabaseon projeksiyonunun dollarToEurotatmin edici olması gerektiği sonucuna varabiliriz . Veya açıklamada " fetchFromDatabaseima saf" dollarToEuroberi (saf fetchFromDatabasebir olan temeli için dollarToEuroskaler faktörü ile x.

Orijinal gönderiden, fetchFromDatabasebunun bir işlev zamanı olduğunu anlayabiliyorum . Bu anlayışı şeffaf hale getirmek için yeniden düzenleme çabasını geliştirelim, böylece açıkça fetchFromDatabasesaf bir işlev olarak nitelendirelim :

fetchFromDatabase = (timestamp) => {/ * burada * /} uygulaması gider;

Sonuçta, özelliği şu şekilde yeniden düzenlerdim:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Sonuç olarak, dollarToEurosadece doğru şekilde çağırdığını fetchFromDatabase(veya türevini exchangeRate) kanıtlayarak birim test edilebilir .


1
Bu çok aydınlatıcıydı. +1. Teşekkürler.
Kardan Adam

Cevabınızı daha bilgilendirici ve belki de belirli bir kullanım durumu için daha iyi yeniden düzenleme yaparken bulurken dollarToEuro; OP'de başka kullanım durumlarının olabileceğinden bahsettim. DollarToEuro'yu seçtim çünkü anında yapmaya çalıştığım şeyi çağrıştırıyor, ancak değişebilen, ancak zamanın bir fonksiyonu olarak değil, özgür bir değişkene bağlı olan daha az ince bir şey olabilir. Bunu göz önünde bulundurarak, üst düzey refactorun daha erişilebilir ve benzer kullanım durumlarında diğerlerine yardımcı olabilecek bir tane olduğunu düşünüyorum. Ne olursa olsun yardımlarınız için teşekkürler.
Kardan Adam

-1

Ben bir Haskell / JS iki dilli ve Haskell, işlev saflığı hakkında büyük bir anlaşma yapan dillerden biri, bu yüzden size Haskell'in nasıl gördüğü perspektifini vereceğimi düşündüm.

Diğerlerinin söylediği gibi, Haskell'de değişebilir bir değişkeni okumak genellikle saf değildir. Değişkenler ve tanımlar arasında bir fark vardır, çünkü değişkenler daha sonra değişebilir, tanımlar sonsuza kadar aynıdır. Eğer Yani eğer sahada onu beyan constsonra (sadece bir varsayarak numbersaf bir tanım kullanılarak olacağını okuma ve yoksayılabilecek iç yapıya sahiptir). Fakat zaman içinde değişen döviz kurlarını modellemek istediniz ve bu bir çeşit değişebilirlik gerektiriyor ve sonra safsızlığa giriyorsunuz.

Haskell'de bu tür saf olmayan şeyleri tanımlamak (onlara “etki” diyebiliriz ve bunların “saf” ın aksine “etkili” kullanımını) tanımlamak için metaprogramlama diyebileceğiniz şeyi yaparız . Bugün metaprogramlama genellikle kastettiğim şey olmayan makrolara atıfta bulunuyor , daha ziyade genel olarak başka bir program yazmak için bir program yazma fikridir.

Bu durumda, Haskell'de, istediğimizi yapacak etkili bir programı hesaplayan saf bir hesaplama yazıyoruz. Yani bir Haskell kaynak dosyasının (en azından kütüphane değil, bir programı tanımlayan bir dosya) bütün mesele denilen etkili bir program için saf hesaplamayı tanımlamaktır main. Daha sonra Haskell derleyicisinin işi, bu kaynak dosyayı almak, o saf hesaplamayı yapmak ve bu etkili programı, daha sonra boş zamanlarınızda çalıştırılacak sabit diskinizde bir yere ikili yürütülebilir olarak koymaktır. Diğer bir deyişle, saf hesaplamanın çalıştığı zaman (derleyici yürütülebilir dosyayı hazırlarken) ve etkili programın çalıştığı zaman (yürütülebilir dosyayı her çalıştırdığınızda) arasında bir boşluk vardır.

Bu nedenle bizim için etkili programlar gerçekten bir veri yapısıdır ve sadece belirtilerek kendiliğinden bir şey yapmazlar (dönüş değerlerine ek olarak * yan- * etkileri yoktur; dönüş değerleri etkilerini içerir). Değişmez programları ve bunlarla yapabileceğiniz bazı şeyleri açıklayan bir TypeScript sınıfının çok hafif bir örneği için,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

Anahtar şudur: Eğer bir Program<x>yan etkiniz yoksa, bunlar tamamen işlevsel olarak saf varlıklardır. Bir fonksiyonun bir program üzerinde eşlenmesi, fonksiyonun saf bir fonksiyon olmadığı sürece herhangi bir yan etkisi yoktur; iki programı sıralamanın herhangi bir yan etkisi yoktur; vb.

Örneğin, bunu durumunuzda nasıl uygulayacağınız gibi, kullanıcıları kimliğe göre almak ve bir veritabanını değiştirmek ve JSON verilerini almak için programları döndüren bazı saf işlevler yazabilirsiniz.

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

ve sonra bir URL'yi kıvırmak ve bazı çalışanları aramak ve amirlerine tamamen işlevsel bir şekilde bildirmek için bir cron işi tanımlayabilirsiniz.

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Mesele şu ki, buradaki her bir işlev tamamen saf bir işlevdir; action.run()onu harekete geçirene kadar hiçbir şey olmadı . Ayrıca,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

JS'nin iptali söz vermiş olsaydı, iki program birbiriyle yarışabilir ve ilk sonucu alıp ikincisini iptal edebiliriz. (Yani hala yapabiliriz, ama ne yapacağımız daha az netleşir.)

Benzer şekilde sizin durumunuzda değişen döviz kurlarını

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

ve exchangeRatebir değişken değerde görünen bir program olabilir,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

ancak öyle olsa bile, bu işlev dollarsToEurosartık sayıdan sayı üreten bir programa kadar saf bir işlevdir ve bu belirleyici eşitlikle herhangi bir yan etkisi olmayan herhangi bir program hakkında mantıksal olarak akıl yürütebilirsiniz.

Maliyet, elbette, sonunda bunu bir .run() yere çağırmanız gerektiğidir ve bu saf olmayacaktır. Ancak, hesaplamanızın tüm yapısı saf bir hesaplama ile tanımlanabilir ve safsızlığı kodunuzun kenar boşluklarına itebilirsiniz.


Bunun neden reddedildiğini merak ediyorum ama demek istediğim hala yanındayım (aslında, Haskell'deki şeylerin varsayılan olarak saf olduğu programları nasıl manipüle edersiniz) ve aşağı oyları memnuniyetle karşılayacağız. Yine de, downvoters bu konuda sevmedikleri şeyleri açıklayan yorumlar bırakmak isterse, onu geliştirmeye çalışabilirim.
CR Drost

Evet, elbette yazarın yanında neden bu kadar çok downvotes olduğunu, ancak tek bir yorum olmadığını merak ediyordum.
Buda Örs
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.