Tamamen işlevsel diller hakkındaki kavram yanılgıları?


39

Sık sık aşağıdaki ifadelere / argümanlara rastlarım:

  1. Saf işlevsel programlama dilleri, yan etkilere izin vermez (ve bu nedenle pratikte çok az kullanışlıdır, çünkü faydalı bir programın, örneğin dış dünyayla etkileşime girdiğinde, yan etkileri vardır).
  2. Saf işlevsel programlama dilleri, durumu koruyan bir program yazmanıza izin vermez (çoğu programda duruma ihtiyaç duyduğunuz için programlamayı çok zorlaştırır).

Fonksiyonel dillerde uzman değilim, ancak bu konular hakkında şu ana kadar anladığım şey bu.

1. noktaya gelince, çevre ile tamamen işlevsel dillerde etkileşime girebilirsiniz, ancak yan etkileri gösteren kodu (örneğin monadik tiplerle Haskell'de) açıkça işaretlemeniz gerekir. Ayrıca, bildiğim kadarıyla, yan etkilerle hesaplama (tahribatlı veri güncelleme), tercih edilen çalışma şekli olmasa da (monadiç tipleri kullanılarak?) Da mümkün olmalıdır.

2. noktaya gelince, bildiğim kadarıyla, birkaç hesaplama adımından (Haskell'de, monadik tipler kullanarak) değerleri değerlendirip durumu temsil edebileceğinizi bildiğim kadarıyla, ancak bunu yaparken pratik bir tecrübem yok ve benim anlayışım belirsiz.

Öyleyse, yukarıdaki iki ifade herhangi bir anlamda doğru mu, yoksa sadece işlevsel diller hakkındaki yanlış anlamalar mı? Kavram yanılgıları ise, nasıl ortaya çıktılar? (1) yan etkileri uygulamak ve (2) devlet ile bir hesaplama yapmak için Haskell deyimsel yolunu gösteren (muhtemelen küçük) bir kod pasajı yazabilir misiniz?


7
Bunun çoğunu 'saf' bir işlevsel dil olarak tanımladığınız şeye bağlı olduğunu düşünüyorum.
jk.

jk: 'Saf' işlevsel dilleri tanımlama probleminden kaçınmak için Haskell anlamında (iyi tanımlanmış) saflığı varsayalım. Hangi koşullar altında fonksiyonel bir dil 'saf' olarak kabul edilebilir, gelecekteki bir sorunun konusu olabilir.
Giorgio

Her iki cevap da netleştirici fikirler içeriyordu ve hangisini kabul edeceğimi seçmek benim için zordu. Ek sözde kod örnekleri nedeniyle sepp2k'nin cevabını kabul etmeye karar verdim.
Giorgio

Yanıtlar:


26

Bu cevabın amaçları için, "tamamen işlevsel dili" tanımlarım, işlevlerin referans olarak şeffaf olduğu, yani aynı işlevi aynı argümanlarla birden çok kez çağırmak her zaman aynı sonuçları verir. Bu, tamamen işlevsel bir dilin genel tanımının olduğuna inanıyorum.

Saf işlevsel programlama dilleri, yan etkilere izin vermez (ve bu nedenle pratikte çok az kullanışlıdır, çünkü faydalı bir programın, örneğin dış dünyayla etkileşime girdiğinde, yan etkileri vardır).

Referans şeffaflığı elde etmenin en kolay yolu gerçekten de yan etkilere izin vermemektir ve aslında durumun olduğu diller vardır (çoğunlukla alana özgü olanlar). Ancak kesinlikle tek yol değildir ve en genel amaç tamamen işlevsel dillerdir (Haskell, Clean, ...) yan etkiye izin vermektedir.

Ayrıca, yan etkisi olmayan bir programlama dilinin pratikte çok az kullanımı olduğunu söyleyerek bence gerçekten adil değil - kesinlikle alana özgü diller için değil, fakat genel amaçlı diller için bile, bir dilin yan etkiler sağlamadan oldukça yararlı olabileceğini hayal ediyorum. . Belki konsol uygulamaları için değil, ama GUI uygulamalarının işlevsel reaktif paradigmada yan etkiler olmadan güzel bir şekilde uygulanabileceğini düşünüyorum.

1. noktaya gelince, çevre ile tamamen işlevsel dillerde etkileşime girebilirsiniz, ancak bunları tanıtan kodu (fonksiyonlar) açıkça işaretlemelisiniz (örneğin monadik tiplerle Haskell'de).

Bu basitleştirmenin biraz üzerinde. Sadece yan etkileme fonksiyonlarının bu şekilde işaretlenmesi gereken bir sisteme sahip olmak (C ++ 'daki yapı-doğruluğuna benzer, ancak genel yan etkilere sahip) referans saydamlığını sağlamak için yeterli değildir. Bir programın aynı argümanlarla hiçbir zaman bir işlevi asla çağıramayacağından ve farklı sonuçlar alacağından emin olmanız gerekir. Bunu ya da böyle şeyler yaparak yapabilirsin.readLinebir işlev olmayan bir şey (Haskell'in IO monad ile yaptığı şeydir) ya da yan etkili işlevleri aynı argümanla birden fazla kez çağırmayı imkansız hale getirebilirsiniz (Clean ne yapar). İkinci durumda, derleyici yan etkileyici bir işlev çağırdığınızda, bunu yeni bir argümanla yaptığınızdan ve aynı argümanı iki kez yan etkileyici işlevine geçirdiğiniz herhangi bir programı reddedeceğinizi garanti eder.

Saf işlevsel programlama dilleri, durumu koruyan bir program yazmanıza izin vermez (çoğu programda duruma ihtiyaç duyduğunuz için programlamayı çok zorlaştırır).

Yine, tamamen işlevsel bir dil, değişken duruma çok iyi bir şekilde izin vermeyebilir; ancak, yukarıda belirtilen yan etkiler ile anlattığım gibi uygularsanız, saf ve hala değişken duruma sahip olmak kesinlikle mümkündür. Gerçekten değişken durum, başka bir yan etki şeklidir.

Bununla birlikte, fonksiyonel programlama dilleri kesinlikle değişken durumları - özellikle de saf olanları - vazgeçiriyor. Ve bunun programlamanın tuhaflaştığını düşünmüyorum - tam tersi. Bazen (ancak çoğu zaman değil) değişken durum, performans veya açıklık kaybı olmadan (Haskell gibi dillerin değişken durum için olanaklara sahip olmasının nedeni budur) önlenemez.

Kavram yanılgıları ise, nasıl ortaya çıktılar?

Bence pek çok insan sadece "bir fonksiyon aynı argümanlar ile çağrıldığında aynı sonucu vermelidir" ifadesini okuyor ve readLinedeğişebilir durumu koruyan bir kod ya da kod uygulamasının mümkün olmadığı sonucuna varıyor . Dolayısıyla, tamamen işlevsel dillerin bu şeyleri referans şeffaflığı bozmadan tanıtmak için kullanabilecekleri "hilelerin" farkında değiller.

Ayrıca değişken durum, fonksiyonel dillerde ağır biçimde cesaret kırıcıdır, bu nedenle, tamamen işlevsel olanlarda hiç izin verilmediğini varsaymak, o kadar da bir sıçrama değildir.

(1) yan etkileri uygulamak ve (2) devlet ile bir hesaplama yapmak için Haskell deyimsel yolunu gösteren (muhtemelen küçük) bir kod pasajı yazabilir misiniz?

İşte Pseudo-Haskell'de, kullanıcıdan bir isim isteyen ve onu selamlayan bir uygulama. Sözde Haskell, henüz icat ettiğim, Haskell'in IO sistemine sahip, ancak daha geleneksel sözdizimi, daha açıklayıcı fonksiyon isimleri kullanan ve no- donotasyonuna sahip olmayan (IO monadının tam olarak nasıl çalıştığından dikkatini dağıtacağı) bir dildir:

greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)

İpucu burada yani readLinetipte bir değerdir IO<String>ve composeMonadtipi bir argüman alır fonksiyonudur IO<T>(bir tür için T) ve tip bir bağımsız değişken alan bir fonksiyonu olan bir bağımsız değişken Tve tip bir değer verir IO<U>(bir tür için U). printbir dize alan ve türün değerini döndüren bir işlevdir IO<void>.

Bir tür değeri, bir tür IO<A>değeri üreten belirli bir işlemi "kodlayan" bir değerdir A. eylemini izleyen eylemi kodlayan composeMonad(m, f)yeni bir değer üretir, eylemini gerçekleştirerek üretilen değerin nerede olduğunu .IOmf(x)xm

Değişken devlet şöyle görünürdü:

counter = mutableVariable(0)
increaseCounter(cnt) =
    setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
    composeMonad(getValue(cnt), setIncreasedValue)

printCounter(cnt) = composeMonad( getValue(cnt), print )

main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )

İşte mutableVariableher türün değerini alan Tve üreten bir işlev MutableVariable<T>. İşlev , geçerli değerini üreten birini getValuealır MutableVariableve döndürür IO<T>. a ve a'yı setValuealır MutableVariable<T>ve değeri ayarlayanı Tdöndürür IO<void>. ilk argümanın mantıklı bir değer üretmeyen ve ikinci argümanın bir monad döndüren bir işlev olmadığı dışında olduğu composeVoidMonadgibi composeMonadaynıdır IO.

Haskell'de, bütün bu sıkıntıyı daha az acı verici kılan bazı sözdizimsel şeker var, ancak değişken durumun, dilin gerçekten yapmanızı istemediği bir şey olduğu açık.


Harika cevap, birçok fikri açıklığa kavuşturmak. Kod pasajının son satırı adı kullanmalı mı counter, yani increaseCounter(counter)?
Giorgio

@ Giorgio Evet, olmalı. Sabit.
sepp2k

1
@Giorgio Görevimde açıkça belirtmeyi unuttuğum bir şey, geri gönderilen IO eyleminin maingerçekte gerçekleşen eylem olacağıdır . Bir GÇ'yi geri döndürmekten mainbaşka, IOeylemleri yürütmenin bir yolu yoktur ( unsafeadlarında olan korkunç kötülük işlevlerini kullanmadan ).
sepp2k

TAMAM. Scarridge ayrıca tahrip edici IOdeğerlerden de bahsetti . Desen eşleşmesine, yani bir cebirsel veri tipinin değerlerini deşifre edebileceğinize, yani değerler eşliğinde bunu yapmak için model eşleştirmesini kullanamayacağınız anlamına geldiğini anlamadım IO.
Giorgio

16

IMHO aklınız karıştı çünkü saf bir dil ile saf bir işlev arasında bir fark var . Fonksiyonla başlayalım. Bir fonksiyon (eğer aynı girdiyi verirse) daima aynı değeri döndürür ve gözlemlenebilir herhangi bir yan etkiye neden olmazsa saftır . Tipik örnekler f (x) = x * x gibi matematiksel fonksiyonlardır. Şimdi bu fonksiyonun bir uygulamasını düşünün. Genelde saf işlevsel dil olarak kabul edilmeyenler bile, örneğin ML gibi birçok dilde saf olurdu. Bu davranışa sahip bir Java veya C ++ yöntemi bile saf olarak kabul edilebilir.

Öyleyse saf dil nedir? Kesin konuşursak, saf bir dilin saf olmayan işlevleri ifade etmenize izin vermemesi beklenebilir. Buna saf dilin idealist tanımı diyelim . Böyle bir davranış oldukça arzu edilir. Neden? Sadece saf fonksiyonlardan oluşan bir program hakkındaki iyi şey, programın anlamını değiştirmeden fonksiyon uygulamasını onun değeri ile değiştirebilmenizdir. Bu, programlar hakkında mantıklı davranmayı çok kolaylaştırır çünkü sonucu bir kere öğrendiğinizde, hesaplanma şeklini unutabilirsiniz. Saflık ayrıca derleyicinin belirli agresif optimizasyonlar yapmasına da izin verebilir.

Peki ya içsel bir duruma ihtiyacınız varsa? Basit bir dilde durumu basitçe, bir girdi parametresi olarak hesaplama öncesi durumu ve sonucun bir parçası olarak hesaplama sonrası durumu ekleyerek taklit edebilirsiniz. Int -> BoolSenin yerine bir şey olsun Int -> State -> (Bool, State). Basitçe bağımlılığı açıkça belirtiyorsunuz (herhangi bir programlama paradigmasında iyi uygulama olarak kabul edilir). BTW, bu tür durum taklit işlevlerini daha büyük durum taklit işlevlerinde birleştirmek için özellikle şık bir yol olan bir monad var. Bu şekilde kesinlikle devleti saf bir dilde tutabilirsiniz. Ama bunu açıkça belirtmelisin.

Yani bu dış ile etkileşime girebileceğim anlamına mı geliyor? Sonuçta, faydalı bir program, yararlı olabilmek için gerçek dünya ile etkileşime girmelidir. Ancak girdi ve çıktı açıkça saf değil. Belirli bir dosyaya belirli bir bayt yazmak, ilk defa iyi olabilir. Ancak tam olarak aynı işlemi yapmak, ikinci kez bir hata verebilir, çünkü disk dolu. Açıkça, bir dosyaya yazabilecek hiçbir saf dil (idealist anlamda) yoktur.

Bu yüzden bir ikilemle karşı karşıyayız. Çoğunlukla saf fonksiyonlar istiyoruz, ancak bazı yan etkiler kesinlikle gerekli ve bunlar saf değil. Şimdi saf bir dilin gerçekçi bir tanımı , saf parçaları diğer parçalardan ayırmak için bazı araçların olması gerektiğidir. Mekanizma, saf olmayan işlemlerin saf parçalara gizlice girmemesini sağlamalıdır.

Haskell'de bu IO tipinde yapılır. Bir IO sonucunu tahrip edemezsiniz (güvensiz mekanizmalar olmadan). Böylece, IO sonuçlarını yalnızca IO modülünde tanımlanan fonksiyonlarla işleyebilirsiniz. Neyse ki, bir GÇ sonucunu almanıza ve bir GÇ sonucunu döndürdüğü sürece bir fonksiyonda işlemenize izin veren çok esnek bir birleştiriciler var. Bu birleştirici ciltleme (veya >>=) olarak adlandırılır ve türüne sahiptir IO a -> (a -> IO b) -> IO b. Bu kavramı genelleştirirseniz monad sınıfına gelirsiniz ve IO bunun bir örneği olur.


4
Haskell'in ( unsafeadına herhangi bir işlevi yok sayarak ) idealist tanımınızı nasıl karşılamadığını gerçekten anlamıyorum . Haskell'de saf olmayan işlevler yoktur (yine yoksayılıyor unsafePerformIOve co.).
sepp2k

4
readFileve aynı argümanlar verilen writeFileher zaman aynı IOdeğeri döndürür. Örneğin, iki kod parçacığını let x = writeFile "foo.txt" "bar" in x >> xve writeFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"aynı şeyi yapacaktır.
sepp2k

3
@ AliiCully "GÇ işlevi" ile neyi kastediyorsunuz? Bir tür değeri döndüren bir işlev IO Something? Öyleyse, bir IO işlevini iki kez aynı argümanla çağırmak mükemmel şekilde mümkündür: putStrLn "hello" >> putStrLn "hello"- burada her ikisi putStrLnde aynı argümana sahip olmak için çağrılar yapar . Tabii ki bu bir sorun değil çünkü daha önce de söylediğim gibi her iki çağrı da aynı G / Ç değeriyle sonuçlanacak.
sepp2k

3
@scarfridge değerlendirilmesi writeFile "foo.txt" "bar"işlev çağrısını değerlendiren çünkü hataya neden olamaz değil eylem yürütmek. Eğer önceki örneğime göre, versiyonun iki letversiyonu varken versiyonun bir IO arızasına neden olma şansı olduğunu letsöylüyorsanız, yanılıyorsunuz. Her iki versiyonda da bir IO hatası için iki fırsat bulunur. Yana letsürüm çağrıyı değerlendirir için writeFilesadece bir kez olmadan versiyon ise letdeğerlendirir ona iki kere, bunu fonksiyon denir ne sıklıkta önemli değil görebilirsiniz. Sadece sonuç ne kadar sık ​​sık önemli ...
sepp2k

6
@AidanCully "Monad mekanizması" örtük parametrelerin etrafından geçmez. putStrLnFonksiyon tipidir tam olarak bir argüman almaktadır String. Bana inanmıyorsanız, türünün bakın: String -> IO (). Kesinlikle herhangi bir tür argümanı almaz IO- bu tür bir değer üretir.
sepp2k
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.