come un linguaggio di programmazione funzionale puro gestisce senza istruzioni di assegnazione?


26

Leggendo il famoso SICP, ho scoperto che gli autori sembrano piuttosto riluttanti a presentare la dichiarazione di incarico a Scheme nel capitolo 3. Ho letto il testo e ho capito perché si sentono così.

Dato che Scheme è il primo linguaggio di programmazione funzionale di cui io abbia mai saputo qualcosa, sono un po 'sorpreso che ci siano alcuni linguaggi di programmazione funzionale (non Scheme ovviamente) che possono fare a meno dei compiti.

Usiamo l'esempio offerto dal libro, l' bank accountesempio. Se non è presente alcuna istruzione di assegnazione, come è possibile farlo? Come modificare la balancevariabile? Lo chiedo perché so che ci sono alcuni cosiddetti linguaggi funzionali puri là fuori e secondo la teoria completa di Turing, anche questo può essere fatto.

Ho imparato C, Java, Python e uso molto i compiti in ogni programma che ho scritto. Quindi è davvero un'esperienza che apre gli occhi. Spero davvero che qualcuno possa spiegare brevemente come vengono evitati gli incarichi in quei linguaggi di programmazione funzionale e quale impatto profondo (se presente) ha su questi linguaggi.

L'esempio sopra menzionato è qui:

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

Questo ha cambiato il balanceby set!. Per me sembra un metodo di classe cambiare il membro della classe balance.

Come ho detto, non ho familiarità con i linguaggi di programmazione funzionale, quindi se ho detto qualcosa di sbagliato su di loro, sentiti libero di sottolineare.


1
Per quanto riguarda l'apprendimento di un linguaggio puramente funzionale: non consiglierei necessariamente di farlo subito. Se impari Haskell, oltre a imparare a scrivere programmi senza variabili mutabili, dovrai anche conoscere la pigrizia e il modo di Haskell di eseguire IO. Potrebbe essere un po 'tutto in una volta. Se vuoi imparare a scrivere programmi senza stato modificabile, il modo più semplice sarebbe probabilmente quello di scrivere un sacco di programmi di schema senza usare set!o altre funzioni che finiscono con a !. Una volta che ti senti a tuo agio con questo, il passaggio a FP puro dovrebbe essere più facile.
sepp2k,

Risposte:


21

Se non è presente alcuna istruzione di assegnazione, come è possibile farlo? Come modificare la variabile di saldo?

Non è possibile modificare le variabili senza una sorta di operatore di assegnazione.

Lo chiedo perché so che ci sono alcuni cosiddetti linguaggi funzionali puri là fuori e secondo la teoria completa di Turing, anche questo può essere fatto.

Non proprio. Se una lingua è Turing completa, ciò significa che può calcolare qualsiasi cosa possa calcolare qualsiasi altra lingua completa di Turing. Ciò non significa che debba avere tutte le funzionalità di altre lingue.

Non è una contraddizione che un linguaggio di programmazione completo di Turing non abbia modo di modificare il valore di una variabile, purché per ogni programma che abbia variabili mutabili, puoi scrivere un programma equivalente che non abbia variabili mutabili (dove "equivalente" significa che calcola la stessa cosa). E in effetti ogni programma può essere scritto in questo modo.

Per quanto riguarda il tuo esempio: in un linguaggio puramente funzionale semplicemente non saresti in grado di scrivere una funzione che restituisce un diverso saldo del conto ogni volta che viene chiamato. Ma saresti ancora in grado di riscrivere ogni programma, che utilizza tale funzione, in modo diverso.


Dato che hai chiesto un esempio, consideriamo un programma imperativo che utilizza la tua funzione make -draw (in pseudo-codice). Questo programma consente all'utente di prelevare da un conto, depositare su di esso o richiedere la quantità di denaro nel conto:

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))

Ecco un modo per scrivere lo stesso programma senza usare variabili mutabili (non mi preoccuperò di IO referenzialmente trasparente perché la domanda non era su questo):

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)

La stessa funzione potrebbe anche essere scritta senza usare la ricorsione usando una piega sull'input dell'utente (che sarebbe più idiomatica della ricorsione esplicita), ma non so se hai ancora familiarità con le pieghe, quindi l'ho scritto in un modo che non usa ancora nulla di sconosciuto.


Posso vedere il tuo punto ma vediamo che voglio un programma che simuli anche il conto bancario e possa anche fare queste cose (prelevare e depositare), allora c'è un modo semplice per farlo?
Gnijuohz,

@Gnijuohz Dipende sempre da quale problema stai esattamente cercando di risolvere. Ad esempio, se si dispone di un saldo iniziale e di un elenco di prelievi e depositi e si desidera conoscere il saldo dopo tali prelievi e depositi, è possibile semplicemente calcolare la somma dei depositi meno la somma dei prelievi e aggiungerla al saldo iniziale . Quindi nel codice sarebbe newBalance = startingBalance + sum(deposits) - sum(withdrawals).
sepp2k,

1
@Gnijuohz Ho aggiunto un programma di esempio alla mia risposta.
sepp2k,

Grazie per il tempo e gli sforzi che hai dedicato a scrivere e riscrivere la risposta! :)
Gnijuohz,

Aggiungo che usare la continuazione potrebbe anche essere un mezzo per raggiungerlo nello schema (fintanto che puoi passare un argomento alla continuazione?)
dader51

11

Hai ragione che assomiglia molto a un metodo su un oggetto. Questo perché è essenzialmente quello che è. La lambdafunzione è una chiusura che tira la variabile esterna balancenel suo ambito. Avere più chiusure che si chiudono sulle stesse variabili esterne e avere più metodi sullo stesso oggetto sono due diverse astrazioni per fare esattamente la stessa cosa, e l'una o l'altra possono essere implementate in termini dell'altra se si capiscono entrambi i paradigmi.

Il modo in cui i linguaggi funzionali puri gestiscono lo stato è imbrogliare. Ad esempio, in Haskell se vuoi leggere l'input da una fonte esterna, (che non è deterministica, ovviamente, e non necessariamente darà lo stesso risultato due volte se lo ripeti), usa un trucco da monade per dire "abbiamo abbiamo questa altra finta variabile che rappresenta lo stato dell'intero resto del mondo , e non possiamo esaminarlo direttamente, ma leggere l'input è una funzione pura che prende lo stato del mondo esterno e restituisce l'input deterministico a quello stato esatto renderà sempre, oltre al nuovo stato del mondo esterno ". (Questa è una spiegazione semplificata, ovviamente. Leggere sul modo in cui funziona effettivamente ti spezzerà il cervello.)

O nel caso del problema del tuo conto bancario, invece di assegnare un nuovo valore alla variabile, può restituire il nuovo valore come risultato della funzione, e quindi il chiamante deve gestirlo in uno stile funzionale, generalmente ricreando tutti i dati che fa riferimento a quel valore con una nuova versione contenente il valore aggiornato. (Questa non è un'operazione così voluminosa come potrebbe sembrare se i tuoi dati sono impostati con il giusto tipo di struttura ad albero.)


Sono davvero interessato alla nostra risposta e all'esempio di Haskell ma a causa della mancanza di conoscenza al riguardo non riesco a comprendere appieno l'ultima parte della tua risposta (beh, anche la seconda parte :()
Gnijuohz

3
@Gnijuohz L'ultimo paragrafo sta dicendo che invece di b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))si può fare solo b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));in cui withdrawè semplicemente definito come withdraw(balance, amount) = balance - amount.
sepp2k,

3

"Operatori ad assegnazione multipla" è un esempio di una caratteristica del linguaggio che, in generale, ha effetti collaterali ed è incompatibile con alcune proprietà utili dei linguaggi funzionali (come la valutazione pigra).

Ciò, tuttavia, non significa che l'assegnazione in generale sia incompatibile con un puro stile di programmazione funzionale (vedi questa discussione per esempio), né significa che non puoi costruire una sintassi che consenta azioni che assomigliano ad assegnazioni in generale, ma sono implementati senza effetti collaterali. Creare quel tipo di sintassi e scrivere programmi efficienti in esso, tuttavia, richiede tempo e è difficile.

Nel tuo esempio specifico, hai ragione: il set! l'operatore è un incarico. È non un operatore libero effetto collaterale, ed è un luogo dove pause Schema con un approccio puramente funzionale alla programmazione.

In ultima analisi, qualsiasi linguaggio puramente funzionale è costretta a rompere con l'approccio a volte puramente funzionale - la stragrande maggioranza dei programmi utili farlo ha effetti collaterali. La decisione su dove farlo è di solito una questione di convenienza e i progettisti linguistici cercheranno di offrire al programmatore la massima flessibilità nel decidere dove rompere con un approccio puramente funzionale, come appropriato per il loro programma e dominio problematico.


"Alla fine, qualsiasi linguaggio puramente funzionale dovrà rompere con l'approccio puramente funzionale qualche volta - la stragrande maggioranza dei programmi utili ha effetti collaterali" Vero, ma poi stai parlando di fare IO e simili. Molti programmi utili possono essere scritti senza variabili mutabili.
sepp2k,

1
... e con "la stragrande maggioranza" di programmi utili, intendi "tutto", giusto? Ho difficoltà anche a immaginare la possibilità dell'esistenza di qualsiasi programma che potrebbe ragionevolmente essere definito "utile" che non esegue l'I / O, un atto che richiede effetti collaterali in entrambe le direzioni.
Mason Wheeler,

I programmi SQL @MasonWheeler non eseguono IO come tale. Inoltre, non è raro scrivere un sacco di funzioni che non eseguono operazioni di IO in una lingua che ha un REPL e quindi semplicemente chiamarle da un REPL. Questo può essere perfettamente utile se il tuo pubblico di destinazione è in grado di utilizzare il REPL (specialmente se il tuo pubblico di destinazione sei tu).
sepp2k,

1
@MasonWheeler: solo un singolo esempio semplice: il calcolo concettuale di n cifre di pi non richiede alcun I / O. È "solo" matematica e variabili. L'unico input richiesto è n e il valore restituito è Pi (a n cifre).
Joachim Sauer,

1
@Joachim Sauer alla fine vorrai stampare il risultato sullo schermo, o riportarlo in altro modo all'utente. E inizialmente vorrai caricare alcune costanti nel programma da qualche parte. Quindi, se vuoi essere pedante, tutti i programmi utili devono fare IO a un certo punto, anche se sono casi banali che sono impliciti e sempre nascosti dal programmatore dall'ambiente
blueberryfields

3

In un linguaggio puramente funzionale, si programmerebbe un oggetto di conto bancario come una funzione di trasformazione del flusso. L'oggetto è considerato come una funzione da un flusso infinito di richieste dai proprietari dell'account (o chiunque) a un flusso potenzialmente infinito di risposte. La funzione inizia con un saldo iniziale ed elabora ciascuna richiesta nel flusso di input per calcolare un nuovo saldo, che viene quindi ricondotto alla chiamata ricorsiva per elaborare il resto del flusso. (Ricordo che la SICP discute il paradigma del trasformatore di flusso in un'altra parte del libro.)

Una versione più elaborata di questo paradigma si chiama "programmazione reattiva funzionale" discussa qui su StackOverflow .

Il modo ingenuo di fare i trasformatori di flusso ha alcuni problemi. È possibile (in effetti, abbastanza semplice) scrivere programmi buggy che mantengano tutte le vecchie richieste in giro, sprecando spazio. Più seriamente, è possibile fare in modo che la risposta alla richiesta corrente dipenda da richieste future. Le soluzioni a questi problemi sono attualmente in fase di elaborazione. Neel Krishnaswami è la forza dietro di loro.

Disclaimer : non appartengo alla chiesa della pura programmazione funzionale. In effetti, non appartengo a nessuna chiesa :-)


Immagino che appartieni a qualche tempio? :-P
Gnijuohz,

1
Il tempio del libero pensiero. Nessun predicatore lì.
Uday Reddy,

2

Non è possibile rendere funzionale un programma al 100% se si prevede che faccia qualcosa di utile. (Se gli effetti collaterali non sono necessari, allora l'intero pensiero potrebbe essere stato ridotto a un tempo di compilazione costante) Come nell'esempio di prelievo puoi rendere funzionali la maggior parte delle procedure ma alla fine avrai bisogno di procedure con effetti collaterali (input dell'utente, uscita su console). Detto questo, puoi rendere funzionale la maggior parte del tuo codice e quella parte sarà facile da testare, anche automaticamente. Quindi si crea un codice imperativo per eseguire l'input / output / database / ... che richiederebbe il debug, ma mantenendo pulita la maggior parte del codice non sarà troppo lavoro. Userò il tuo esempio di prelievo:

(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))

È possibile fare lo stesso in quasi tutte le lingue e produrre gli stessi risultati (meno bug), anche se potrebbe essere necessario impostare variabili temporanee all'interno di una procedura e persino mutare roba, ma questo non importa tanto quanto la procedura in realtà agisce in modo funzionale (i soli parametri determinano il risultato). Credo che diventerai un programmatore migliore in qualsiasi lingua dopo aver programmato un po 'di LISP :)


+1 per l'esempio esaustivo e le spiegazioni realistiche sulle parti funzionali e le parti funzionali non pure del programma e menzionando perché i PQ contano comunque.
Zelphir Kaltstahl,

1

L'assegnazione è una cattiva operazione perché divide lo spazio degli stati in due parti, prima dell'assegnazione e dopo l'assegnazione. Ciò causa difficoltà nel tenere traccia di come le variabili vengono modificate durante l'esecuzione del programma. Le seguenti cose nei linguaggi funzionali stanno sostituendo i compiti:

  1. Parametri funzione collegati direttamente ai valori restituiti
  2. scegliendo diversi oggetti da restituire invece di modificare oggetti esistenti.
  3. creando nuovi valori valutati in modo pigro
  4. elencando tutti gli oggetti possibili , non solo quelli che devono essere in memoria
  5. nessun effetto collaterale

Questo non sembra rispondere alla domanda posta. Come si programma un oggetto di conto bancario in un linguaggio funzionale puro?
Uday Reddy,

sono solo funzioni che si trasformano da un conto bancario all'altro. La chiave è che quando si verificano tali trasformazioni, vengono scelti nuovi oggetti anziché modificare quelli esistenti.
tp1,

Quando si trasforma un record di un conto bancario in un altro, si desidera che il cliente esegua la transazione successiva sul nuovo record, non su quello precedente. Il "punto di contatto" per il cliente deve essere costantemente aggiornato per puntare al record corrente. Questa è un'idea fondamentale di "modifica". Gli "oggetti" del conto bancario non sono record del conto bancario.
Uday Reddy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.