Tamamen işlevsel bir programlama dili atama cümleleri olmadan nasıl yönetilir?


26

Ünlü SICP'yi okurken, yazarların 3. bölümdeki atama ifadesini Scheme'ye sunmakta isteksiz olduklarını gördüm. Metinleri ve neden böyle hissettiklerini anlıyorum.

Scheme, hakkında bir şey bildiğim ilk fonksiyonel programlama dili olduğundan, bazı işlevsel programlama dillerinin (elbette Scheme değil) atamalar olmadan yapabileceği konusunda şaşırdım.

Kitabın sunduğu örneği, örnek olarak kullanalım bank account. Ödev ifadesi yoksa, bu nasıl yapılabilir? balanceDeğişkeni nasıl değiştirebilirim ? Öyle soruyorum çünkü orada saf denilen bazı işlevsel diller olduğunu biliyorum ve Turing'in tam teorisine göre, bu da yapılabilir.

C, Java, Python öğrendim ve yazdığım her programda ödevleri çok kullandım. Bu yüzden gerçekten göz açıcı bir deneyim. Umarım birileri bu işlevsel programlama dillerinde görevlendirmelerden nasıl kaçınıldığını ve bu diller üzerinde (varsa) ne kadar etkili olduğunu kısaca açıklayabilir.

Yukarıda belirtilen örnek burada:

(define (make-withdraw balance)
    (lambda (amount)
        (if (>= balance amount)
            (begin (set! balance (- balance amount))
                balance)
            "Insufficient funds")))

Bu balancetarafından değiştirildi set!. Bana göre sınıf üyesini değiştirmek için sınıf yöntemine çok benziyor balance.

Dediğim gibi, işlevsel programlama dillerine aşina değilim, bu yüzden onlar hakkında yanlış bir şey söylersem, söylemekten çekinmeyin.


1
Tamamen işlevsel bir dil öğrenmeye gelince: Bunu hemen yapmayı kesinlikle tavsiye etmem. Haskell'i öğrenirseniz, değişken değişkenleri olmayan programları yazmayı öğrenmenin yanı sıra, tembellik ve Haskell'in IO gerçekleştirme yolunu da öğrenmeniz gerekir. Bu seferde biraz fazla olabilir. Değişken durum olmadan programları yazmayı öğrenmek istiyorsanız, en kolay yol muhtemelen kullanmadan set!veya a ile biten diğer işlevleri kullanmadan bir sürü program programı yazmak olacaktır !. Bundan memnun olduğunuzda, saf FP'ye geçiş daha kolay olmalıdır.
sepp2k

Yanıtlar:


21

Ödev ifadesi yoksa, bu nasıl yapılabilir? Bakiye değişkeni nasıl değiştirilir?

Bir çeşit atama operatörü olmadan değişkenleri değiştiremezsiniz.

Öyle soruyorum çünkü orada saf denilen bazı işlevsel diller olduğunu biliyorum ve Turing'in tam teorisine göre, bu da yapılabilir.

Tam değil. Eğer bir dil Turing tamamlandıysa, başka bir Turing complete dilinin hesaplayabileceği her şeyi hesaplayabileceği anlamına gelir. Diğer dillerin sahip olduğu her özelliğe sahip olması gerektiği anlamına gelmez.

Turing'in eksiksiz bir programlama dilinin değişken değişkenin değerini değiştirmenin hiçbir yolu olmadığı, değişken değişkenleri olan her program için, değişken değişkenleri olmayan eşdeğer bir program yazabileceğiniz anlamına gelmez ("eşdeğer" anlamına gelir). Aynı şeyi hesaplar). Ve aslında her program bu şekilde yazılabilir.

Örneğinizle ilgili olarak: Tamamen işlevsel bir dilde, her çağrıldığında farklı bir hesap bakiyesi döndüren bir işlev yazamazsınız. Ancak, böyle bir işlevi kullanan her programı farklı şekilde yeniden yazabilirsiniz.


Bir örnek istediğinden beri, make-pull fonksiyonunu kullanan zorunlu bir programı ele alalım (sözde kodda). Bu program, kullanıcının bir hesaptan para çekmesine, para yatırmasına veya hesaptaki para miktarını sorgulamasına izin verir:

account = make-withdraw(0)
ask for input until the user enters "quit"
    if the user entered "withdraw $x"
        account(x)
    if the user entered "deposit $x"
        account(-x)
    if the user entered "query"
        print("The balance of the account is " + account(0))

İşte aynı programı değişken değişken kullanmadan yazmanın bir yolu:

function IO_loop(balance):
    ask for input
    if the user entered "withdraw $x"
        IO_loop(balance - x)
    if the user entered "deposit $x"
        IO_loop(balance + x)
    if the user entered "query"
        print("The balance of the account is " + balance)
        IO_loop(balance)
    if the user entered "quit"
        do nothing

 IO_loop(0)

Aynı işlev, kullanıcı girdisi üzerinde bir katlama kullanarak (özyinelemeden açık bir ifadeden ziyade aptalca olur), özyineleme kullanmadan da yazılabilir, ancak henüz katlanmaya aşina olup olmadığınızı bilmiyorum. Bu henüz bilmediğin bir şeyi kullanmadı.


Amacınızı görebiliyorum ancak banka hesabını da simüle eden bir program yapmak istiyorum ve bunları da yapabilirim (para çekme ve para yatırma), o zaman bunu yapmanın kolay bir yolu var mı?
Gnijuohz

@Gnijuohz Her zaman tam olarak çözmeye çalıştığınız sorunu seçin. Örneğin, bir başlangıç ​​bakiyeniz ve para çekme ve yatırma listeniz varsa ve bu para yatırma ve yatırma işleminden sonra bakiyeyi bilmek istiyorsanız, para yatırmaların toplamını eksi para çekme toplamını hesaplayabilir ve başlangıç ​​bakiyesine ekleyebilirsiniz. . Yani bu kod olurdu newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k

1
@Gnijuohz Cevaplarıma örnek bir program ekledim.
sepp2k

Cevabı yazmaya ve tekrar yazmaya harcadığınız zaman ve emeğiniz için teşekkür ederiz! :)
Gnijuohz


11

Bir nesnede bir yönteme çok benziyor olması haklısın. Çünkü esasen budur. lambdaİşlevi dış değişken çeken bir kapaktır balancekapsamı içine. Aynı dış değişken (ler) in üzerine kapanan ve aynı nesnede birden fazla yönteme sahip olan birden fazla kapamaya sahip olmak, aynı şeyi yapmak için iki farklı soyutlamadır ve her ikisi de paradigmaları anlarsanız, her ikisi de diğeri açısından uygulanabilir.

Saf işlevsel dillerin durumu ele alması, hile yapmaktır. Mesela Haskell'de eğer harici bir kaynaktan girdi okumak istiyorsanız, (ki bu elbette nodeterministik değildir ve mutlaka aynı sonucu iki kez vermeyecektir), "biz yaptık tüm dünyanın geri kalanının durumunu temsil eden bu başka bir değişkene sahibiz ve doğrudan inceleyemeyiz, ancak girdiyi okumak dış dünyanın durumunu alan ve bu kesin durumun belirleyici verimini döndüren saf bir işlevdir. Daima dış dünyadaki yeni durumu artıracak. ” (Bu basitleştirilmiş bir açıklama, elbette. Aslında işe yaradığını okumak beyninizi ciddi şekilde kıracak.)

Veya banka hesabınızdaki bir problemde, değişkene yeni bir değer atamak yerine, yeni değeri işlev sonucu olarak geri getirebilir ve daha sonra arayan kişi, genellikle herhangi bir veriyi yeniden oluşturarak işlevsel bir tarzda ele almalıdır. bu değer, güncellenmiş değeri içeren yeni bir sürümle birlikte verilir. (Bu, verileriniz doğru türdeki ağaç yapısına ayarlanmışsa, ses verebileceğiniz kadar hantal bir işlem değildir.)


Cevabımızla ve Haskell örneğiyle gerçekten ilgileniyorum ancak bu konuda bilgi sahibi olmadığım için cevabınızın son kısmını tam olarak anlayamıyorum (iyi, ayrıca ikinci kısım :()
Gnijuohz

3
@Gnijuohz Son paragraf, b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))senin yerine , sadece basitçe tanımlandığı b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));yerde yapabileceğini söylüyor . withdrawwithdraw(balance, amount) = balance - amount
sepp2k

3

"Birden fazla atama operatörleri", genel olarak konuşursak, yan etkileri olan ve işlevsel dillerin bazı yararlı özellikleri (uyumsuz değerlendirme gibi) ile uyumsuz olan bir dil özelliğinin bir örneğidir.

Bununla birlikte, bu genel olarak atamanın saf bir işlevsel programlama stili ile uyumsuz olduğu anlamına gelmez ( örneğin bu tartışmaya bakın ), ayrıca genel olarak atamaya benzeyen eylemlere izin veren sözdizimi oluşturamayacağınız anlamına da gelmez. yan etkileri olmadan uygulanır. Bu tür bir sözdizimi oluşturmak ve içinde verimli programlar yazmak, zaman alıcı ve zordur.

Özel örneğinizde haklısınız - set! Operatör olan bir atama. Bu var olmayan bir yan etkisi ücretsiz operatör ve bir yerdir nerede programlamaya tamamen işlevsel bir yaklaşımla Şeması kırılıyor.

Sonuçta, herhangi tamamen işlevsel dil tamamen işlevsel yaklaşım bazen ile kırmak zorunda olacak - yararlı programların büyük çoğunluğu yapmak yan etkileri vardır. Nerede yapılacağına dair karar genellikle bir kolaylık meselesidir ve dil tasarımcıları, programcıya programlarına ve sorunlu alanlarına uygun olarak tamamen işlevsel bir yaklaşımı nereye eriteceklerine karar vermede en yüksek esnekliği vermeye çalışacaktır.


“Sonuçta, herhangi bir tamamen işlevsel dilin bir zamanlar tamamen işlevsel yaklaşımdan kopması gerekecek - faydalı programların büyük çoğunluğunun yan etkileri var” Doğru, ama sonra IO ve benzeri şeyler hakkında konuşuyorsunuz. Değişken değişkenler olmadan birçok faydalı program yazılabilir.
sepp2k

1
... ve faydalı programların "büyük çoğunluğu" ile, "hepsi" mi demek istiyorsunuz? Her iki yönde de yan etkiler gerektiren bir eylem olan G / Ç işlemi gerçekleştirmeyen makul olarak "kullanışlı" olarak adlandırılabilecek herhangi bir programın var olma ihtimalini hayal etmekte bile zorlanıyorum.
Mason Wheeler

@MasonWheeler SQL programları bu şekilde IO yapmaz. Ayrıca, bir REPL olan bir dilde IO yapmayan ve daha sonra da sadece bir REPL'den çağıran fonksiyonlar yazmak pek de yaygın değildir. Bu, hedef kitlenizin REPL özelliğini kullanabiliyorsa, özellikle yararlı bir hedef kitlenizse, kullanışlıdır.
sepp2k

1
@MasonWheeler: sadece tek, basit bir karşı örnek: pi'nin n basamağının kavramsal olarak hesaplanması herhangi bir G / Ç gerektirmez. "Sadece" matematik ve değişkenler. Gerekli olan giriş n , dönüş değeri ise Pi ( n rakamına kadar).
Joachim Sauer

1
@ Joachim Sauer sonunda sonucu ekrana yazdırmak ya da kullanıcıya bildirmek istersiniz. Ve başlangıçta programa bir yerden bazı sabitler yüklemek isteyeceksiniz. Öyleyse, bilgili olmak istersen, tüm faydalı programların belirli bir noktada GÇ yapmaları gerekir , her ne kadar çevre açısından önemsiz ve programlayıcıdan daima gizlenmiş önemsiz durumlar olsa bile
blueberryfields

3

Tamamen işlevsel bir dilde, bir banka hesabı nesnesini bir akım trafosu işlevi olarak programlamak mümkündür. Nesne, hesap sahiplerinden (veya kimseden) sınırsız bir istek akışından potansiyel olarak sınırsız yanıt akışına kadar bir işlev olarak kabul edilir. İşlev bir başlangıç ​​bakiyesiyle başlar ve yeni bir bakiye hesaplamak için giriş akımındaki her isteği işler; bu durumda daha sonra akımın kalanını işlemek için özyinelemeli çağrıya geri gönderilir. (SICP'nin kitabın başka bir bölümünde akım trafosu paradigmasını tartıştığını hatırlıyorum.)

Bu paradigmanın daha ayrıntılı bir sürümüne burada StackOverflow'ta tartışılan "fonksiyonel reaktif programlama" denir .

Akarsu trafosunu yapmanın saf yolunun bazı problemleri vardır. Tüm eski istekleri yerinde tutan ve boşa harcayan buggy programları yazmak mümkün (aslında, oldukça kolay). Daha ciddi olarak, mevcut talebe cevabın gelecekteki taleplere bağlı olarak yapılması mümkündür. Bu sorunların çözümleri şu anda üzerinde çalışılmaktadır. Neel Krishnaswami onların arkasındaki güçtür.

Yasal Uyarı : Ben saf işlevsel programlama kilisesine ait değilim. Aslında, hiçbir kiliseye ait değilim :-)


Sanırım bir tapınağa aitsin? :-P
Gnijuohz

1
Özgür düşünce tapınağı. Orada vaiz yok.
Uday Reddy

2

Yararlı bir şey yapması gerekiyorsa, bir programı% 100 işlevsel hale getirmek mümkün değildir. (Eğer yan etkilere ihtiyaç duyulmazsa, o zaman bütün düşünce sabit bir derleme süresine indirgenmiş olabilir) Çekme örneği gibi işlemlerin çoğunu işlevsel hale getirebilirsiniz, ancak sonuçta yan etkileri olan işlemlere ihtiyacınız olacaktır (kullanıcıdan giriş, konsola çıkışı). Bu, kodunuzun çoğunu işlevsel hale getirebileceğinizi ve bu bölümün otomatik olarak bile test edilmesi kolay olacağını belirtti. O zaman hata ayıklamaya ihtiyaç duyan girdi / çıktı / veritabanı / ... girişini yapmak için bazı zorunlu kodlar yaparsınız, ancak kodun çoğunu temiz tutmak çok fazla çalışmaz. Para çekme örneğini kullanacağım:

(define +no-founds+ "Insufficient funds")

;; functional withdraw
(define (make-withdraw balance amount)
    (if (>= balance amount)
        (- balance amount)
        +no-founds+))

;; functional atm loop
(define (atm balance thunk)
  (let* ((amount (thunk balance)) 
         (new-balance (make-withdraw balance amount)))
    (if (eqv? new-balance +no-founds+)
        (cons +no-founds+ '())
        (cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))

;; functional balance-line -> string 
(define (balance->string x)
  (if (eqv? x +no-founds+)
      (string-append +no-founds+ "\n")
      (if (null? x)
          "\n"
          (let ((first-token (car x)))
            (string-append
             (cond ((symbol? first-token) (symbol->string first-token))
                   (else (number->string first-token)))
             " "
             (balance->string (cdr x)))))))

;; functional thunk to test  
(define (input-10 x) 10) ;; define a purly functional input-method

;; since all procedures involved are functional 
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))

;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!

;; A procedure to get input from user is needed. 
;; Side effects makes it imperative
(define (user-input balance)
  (display "You have $")
  (display balance)
  (display " founds. How much to withdraw? ")
  (read))

;; We need a procedure to print stuff to the console 
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
  (for-each (lambda (x) (display (balance->string x))) x))

;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))

Hemen hemen her dilde aynısını yapmak ve aynı sonuçları üretmek mümkündür (daha az hata), ancak bir prosedür içinde geçici değişkenler ayarlamanız ve hatta bazı şeyleri mutasyona uğratmanız gerekebilir, ancak işlem sürece bu kadar önemli değil aslında işlevseldir (sadece parametreler sonucu belirler). Biraz LISP programladıktan sonra herhangi bir dilde daha iyi bir programcı olduğunuza inanıyorum :)


Fonksiyonel kısımlar ve programın saf olmayan fonksiyonel kısımları hakkında kapsamlı bir örnek ve gerçekçi açıklamalar için +1 ve FP'nin neden önemli olduğunu söyleyerek.
Zelphir Kaltstahl

1

Atama kötü bir işlemdir, çünkü durum alanını iki parçaya ayırır, atama öncesi ve atama sonrası. Bu, program yürütme sırasında değişkenlerin nasıl değiştiğini izlemekte güçlüklere neden olur. İşlevsel dillerde aşağıdakiler atamaları değiştiriyor:

  1. Doğrudan dönüş değerlerine bağlı fonksiyon parametreleri
  2. mevcut nesneleri değiştirmek yerine döndürülecek farklı nesneleri seçmek.
  3. yeni tembel olarak değerlendirilmiş değerler yaratmak
  4. sadece bellekte olması gerekenleri değil tüm olası nesneleri listelemek
  5. yan efektleri olmayan

Bu, sorulan soruyu ele almıyor gibi görünüyor. Bir banka hesabı nesnesini tamamen işlevsel bir dilde nasıl programlıyorsunuz?
Uday Reddy,

bu sadece bir banka hesap rekorundan diğerine dönüşüm fonksiyonları. Anahtar, bu tür dönüşümler gerçekleştiğinde, var olanları değiştirmek yerine yeni nesnelerin seçilmesidir.
tp1

Bir banka hesabı kaydını diğerine dönüştürdüğünüzde, müşterinin bir sonraki işlemi eskisine değil yeni kayda yapmasını istersiniz. Müşteri için "temas noktası" mevcut kayda işaret edecek şekilde sürekli güncellenmelidir. Bu "değişiklik" konusunda temel bir fikirdir. Banka hesabı "nesneler" banka hesap kayıtları değil.
Uday Reddy
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.