Puro funzionale vs dire, non chiedere?


14

"Il numero ideale di argomenti per una funzione è zero" è chiaramente sbagliato. Il numero ideale di argomenti è esattamente il numero necessario per consentire alla funzione di essere libera da effetti collaterali. Meno di questo e causi inutilmente che le tue funzioni siano impure, costringendoti così ad allontanarti dalla fossa del successo e ad arrampicarti sul gradiente del dolore. A volte "Zio Bob" è perfetto con il suo consiglio. A volte ha torto spettacolare. Il suo consiglio a zero argomenti è un esempio di quest'ultimo

( Fonte: commento di @ David Arno su un'altra domanda su questo sito )

Il commento ha ottenuto una quantità spettacolare di 133 voti positivi, motivo per cui mi piacerebbe prestare maggiore attenzione al suo merito.

Per quanto ne so, ci sono due modi separati nella programmazione: pura programmazione funzionale (ciò che questo commento è incoraggiante) e dire, non chiedere (che di tanto in tanto viene raccomandato anche su questo sito Web). AFAIK questi due principi sono fondamentalmente incompatibili, vicini all'opposto dell'altro: il puro funzionale può essere sintetizzato come "solo valori di ritorno, non ha effetti collaterali" mentre dire, non chiedere può essere riassunto come "non restituire nulla, hanno solo effetti collaterali ". Inoltre, sono un po 'perplesso perché pensavo che dire, non chiedere era considerato il nucleo del paradigma OO mentre le funzioni pure erano considerate il nucleo del paradigma funzionale - ora vedo le funzioni pure raccomandate in OO!

Suppongo che gli sviluppatori dovrebbero probabilmente scegliere uno di questi paradigmi e attenersi ad esso? Beh, devo ammettere che non avrei mai potuto nemmeno seguirmi. Spesso mi sembra conveniente restituire un valore e non riesco davvero a vedere come posso ottenere ciò che voglio ottenere solo con effetti collaterali. Spesso mi sembra conveniente avere effetti collaterali e non riesco davvero a vedere come posso ottenere ciò che voglio ottenere solo restituendo valori. Inoltre, spesso (suppongo sia orribile) ho metodi che fanno entrambi.

Tuttavia, da questi 133 giudizi sto ragionando sul fatto che attualmente la pura programmazione funzionale sta "vincendo" in quanto diventa un consenso che è superiore dire, non chiedere. È corretto?

Pertanto, sull'esempio di questo gioco pieno di antipasti che sto cercando di realizzare : se volessi renderlo conforme al puro paradigma funzionale - COME ?!

Mi sembra ragionevole avere uno stato di battaglia. Dato che si tratta di un gioco a turni, tengo gli stati di battaglia in un dizionario (multiplayer - potrebbero esserci molte battaglie giocate da molti giocatori contemporaneamente). Ogni volta che un giocatore fa il suo turno, chiamo un metodo appropriato sullo stato di battaglia che (a) modifica lo stato di conseguenza e (b) restituisce gli aggiornamenti ai giocatori, che vengono serializzati in JSON e sostanzialmente dicono loro cosa è appena successo sul tavola. Questo, suppongo, è in una flagrante violazione di ENTRAMBI i principi e allo stesso tempo.

OK - Potrei fare in modo che un metodo RITORNA sia uno stato di battaglia invece di modificarlo se volessi davvero. Ma! Dovrò quindi copiare inutilmente tutto nello stato di battaglia solo per restituire uno stato completamente nuovo invece di modificarlo sul posto?

Ora forse se la mossa è un attacco potrei semplicemente restituire un personaggio aggiornato HP? Il problema è che non è così semplice: le regole del gioco, una mossa possono e spesso avranno molti più effetti rispetto alla semplice rimozione di una parte degli HP di un giocatore. Ad esempio, può aumentare la distanza tra i personaggi, applicare effetti speciali, ecc.

Mi sembra molto più semplice modificare lo stato in atto e restituire gli aggiornamenti ...

Ma come potrebbe affrontare un ingegnere esperto?


9
Seguire qualsiasi paradigma è un modo sicuro per fallire. La politica non dovrebbe mai superare l'intelligenza. La soluzione a un problema dovrebbe dipendere dal problema, non dalle tue convinzioni religiose sulla risoluzione dei problemi.
John Douma,

1
Non ho mai fatto una domanda qui su qualcosa che ho detto prima. Sono onorato. :)
David Arno

Risposte:


14

Come la maggior parte degli aforismi di programmazione, "dì, non chiedere" sacrifica chiarezza per ottenere brevità. Non è affatto inteso a sconsigliare di chiedere i risultati di un calcolo, si consiglia di non richiedere gli input di un calcolo. "Non ottenere, quindi calcola, quindi imposta, ma va bene restituire un valore da un calcolo" non è così conciso.

Era abbastanza comune per le persone chiamare un getter, fare qualche calcolo su di esso, quindi chiamare un setter con il risultato. Questo è un chiaro segno che il tuo calcolo appartiene effettivamente alla classe su cui hai chiamato il getter. "Tell, Don't Ask" è stato coniato per ricordare alle persone di essere alla ricerca di quell'anti-pattern, e ha funzionato così bene che ora alcune persone pensano che parte sia ovvia, e cercano altri tipi di "chiede" di eliminare. Tuttavia, l'aforisma si applica utilmente solo a quella situazione.

I programmi funzionali puri non hanno mai sofferto di quell'esatto anti-pattern, per la semplice ragione che non ci sono setter in quello stile. Tuttavia, il problema più generale (e più difficile da vedere) di non mescolare diversi livelli di astrazione semantica nella stessa funzione si applica a ogni paradigma.


Grazie per aver spiegato correttamente "Dillo, non chiedere".
user949300

13

Sia lo zio Bob che David Arno (l'autore della citazione che hai avuto) hanno lezioni importanti che possiamo trarre da ciò che hanno scritto. Penso che valga la pena imparare la lezione e poi estrapolare cosa significhi davvero per te e il tuo progetto.

Primo: lezione di zio Bob

Lo zio Bob sta sottolineando che più argomenti hai nella tua funzione / metodo, più gli sviluppatori che lo usano devono capire. Quel carico cognitivo non viene gratis, e se non sei coerente con l'ordine degli argomenti, ecc. Il carico cognitivo aumenta.

Questo è un fatto di essere umani. Penso che l'errore chiave nel libro di codice pulito di zio Bob sia l'affermazione "Il numero ideale di argomenti per una funzione è zero" . Il minimalismo è eccezionale fino a quando non lo è. Proprio come non raggiungi mai i tuoi limiti in Calculus, non raggiungerai mai il codice "ideale", né dovresti.

Come diceva Albert Einstein, "Tutto dovrebbe essere il più semplice possibile, ma non più semplice".

Secondo: lezione di David Arno

Il modo di sviluppare descritto da David Arno è lo sviluppo di uno stile più funzionale rispetto agli oggetti . Tuttavia, il codice funzionale si adatta molto meglio della tradizionale programmazione orientata agli oggetti. Perché? A causa del blocco. Ogni volta che lo stato è mutabile in un oggetto, si corre il rischio di condizioni di gara o di blocco della contesa.

Avendo scritto sistemi altamente concorrenti utilizzati in simulazioni e altre applicazioni lato server, il modello funzionale fa miracoli. Posso attestare i miglioramenti apportati dall'approccio. Tuttavia, è uno stile di sviluppo molto diverso, con requisiti e modi di dire diversi.

Lo sviluppo è una serie di compromessi

Conosci la tua applicazione meglio di chiunque di noi. Potrebbe non essere necessaria la scalabilità fornita con la programmazione di stili funzionali. C'è un mondo tra i due ideali sopra elencati. Quelli di noi che hanno a che fare con sistemi che devono gestire un throughput elevato e un parallelismo ridicolo tenderanno all'ideale della programmazione funzionale.

Detto questo, è possibile utilizzare gli oggetti dati per contenere l'insieme di informazioni necessarie per passare a un metodo. Questo aiuta con il problema del carico cognitivo che lo zio Bob stava affrontando, pur sostenendo l'ideale funzionale a cui David Arno si stava rivolgendo.

Ho lavorato su entrambi i sistemi desktop con parallelismo limitato richiesto e software di simulazione ad alto rendimento. Hanno esigenze molto diverse. Posso apprezzare un codice orientato agli oggetti ben scritto progettato attorno al concetto di nascondere i dati che conosci. Funziona per diverse applicazioni. Tuttavia, non funziona per tutti.

Chi ha ragione? Bene, David ha più ragione di zio Bob in questo caso. Tuttavia, il punto sottostante che voglio sottolineare qui è che un metodo dovrebbe avere tanti argomenti quanti ne abbiano senso.


C'è paralelismo. Diverse battaglie possono essere elaborate in parallelo. Tuttavia, sì: una singola battaglia, mentre viene elaborata, deve essere bloccata.
Gaazkam

Sì, intendevo dire che i lettori (mietitori nella tua analogia) avrebbero tratto spunto dai loro scritti (il seminatore). Detto questo, sono tornato a guardare alcune cose che ho scritto in passato e ho o imparato di nuovo qualcosa o non sono d'accordo con il mio precedente io. Stiamo tutti imparando ed evolvendo, e questa è la ragione numero uno per cui dovresti sempre ragionare su come e se applichi qualcosa che hai imparato.
Berin Loritsch,

8

OK - Potrei fare in modo che un metodo RITORNA sia uno stato di battaglia invece di modificarlo se volessi davvero.

Sì, questa è l'idea.

Dovrò quindi copiare tutto nello stato di battaglia per restituire uno stato completamente nuovo invece di modificarlo sul posto?

No. Il tuo "stato di battaglia" potrebbe essere modellato come una struttura di dati immutabile, che contiene altre strutture di dati immutabili come blocchi predefiniti, forse nidificati in alcune gerarchie di strutture di dati immutabili.

Quindi potrebbero esserci parti dello stato di battaglia che non devono essere cambiate durante un turno, e altre che devono essere cambiate. Le parti che non cambiano non devono essere copiate, poiché sono immutabili, è sufficiente copiare un riferimento a quelle parti, senza alcun rischio di introdurre effetti collaterali. Funziona meglio in ambienti linguistici raccolti in modo inutile.

Google per "Efficient Immutable Data Structures", e troverai sicuramente dei riferimenti su come funziona in generale.

Mi sembra molto più semplice modificare lo stato in atto e restituire gli aggiornamenti.

Per alcuni problemi, questo può essere davvero più semplice. I giochi e le simulazioni a round possono rientrare in questa categoria, dato che gran parte dello stato del gioco cambia da un round all'altro. Tuttavia, la percezione di ciò che è veramente "più semplice" è in una certa misura soggettiva e dipende anche molto da ciò a cui le persone sono abituate.


8

Come autore del commento, immagino che dovrei chiarirlo qui, poiché ovviamente c'è di più rispetto alla versione semplificata che offre il mio commento.

AFAIK questi due principi sono fondamentalmente incompatibili, quasi opposti l'uno all'altro: il puro funzionale può essere sintetizzato come "solo valori di ritorno, non hanno effetti collaterali" mentre dire, non chiedere può essere riassunto come "non restituire nulla, hanno solo effetti collaterali ".

Ad essere sincero, trovo questo un uso davvero strano del termine "dillo, non chiedere". Così ho letto ciò che Martin Fowler ha detto sull'argomento qualche anno fa, il che è stato illuminante . Il motivo per cui l'ho trovato strano è perché "non dire" è sinonimo di iniezione di dipendenza nella mia testa e la forma più pura di iniezione di dipendenza sta passando tutto ciò di cui una funzione ha bisogno attraverso i suoi parametri.

Ma sembra che il significato che applico per "dire non chiedere" deriva dal prendere la definizione focalizzata sull'OO di Fowler e renderla più paradigma agnostica. Nel processo, credo che porti il ​​concetto alle sue logiche conclusioni.

Torniamo agli inizi semplici. Abbiamo "grumi di logica" (procedure) e abbiamo dati globali. Le procedure leggono i dati direttamente per accedervi. Abbiamo un semplice scenario "chiedi".

Vento in avanti un po '. Ora abbiamo oggetti e metodi. I dati non devono più essere globali, possono essere passati tramite il costruttore e contenuti all'interno dell'oggetto. E poi abbiamo metodi che agiscono su quei dati. Quindi ora abbiamo "dillo, non chiedere" come lo descrive Fowler. All'oggetto vengono comunicati i suoi dati. Tali metodi non devono più richiedere l'ambito globale per i loro dati. Ma ecco il problema: questo non è ancora vero "dillo, non chiedere" dal mio punto di vista poiché quei metodi devono ancora chiedere l'ambito dell'oggetto. Questo è più uno scenario "dillo, poi chiedi" che provo.

Quindi prosegui fino ai giorni nostri, scarica l'approccio "è OO fino in fondo" e prendi in prestito alcuni principi dalla programmazione funzionale. Ora quando viene chiamato un metodo, tutti i dati vengono forniti tramite i suoi parametri. Si può (e si è) sostenuto: "Qual è il punto, è solo complicando il codice?" E sì, passando attraverso parametri, i dati accessibili tramite l'ambito dell'oggetto, aggiungono complessità al codice. Ma la memorizzazione di tali dati in un oggetto, anziché renderli accessibili a livello globale, aumenta anche la complessità. Eppure pochi sostengono che le variabili globali siano sempre migliori perché più semplici. Il punto è che i benefici che "dire, non chiedere" portano superano quella complessità della riduzione della portata. Ciò si applica più al passaggio tramite parametri che alla limitazione dell'ambito all'oggetto.private statice passa tutto ciò di cui ha bisogno tramite i parametri e ora quel metodo può essere attendibile per non accedere furtivamente alle cose che non dovrebbe. Inoltre, incoraggia a mantenere piccolo il metodo, altrimenti l'elenco dei parametri sfugge di mano. E incoraggia i metodi di scrittura che si adattano ai criteri della "funzione pura".

Quindi non vedo "puro funzionale" e "dire, non chiedere" come opposti l'uno all'altro. La prima è l'unica piena attuazione della seconda per quanto mi riguarda. L'approccio di Fowler non è completo "dillo, non chiedere".

Ma è importante ricordare che questa "piena attuazione di non chiedere" è davvero un ideale, vale a dire che il pragmatismo deve entrare in gioco meno che diventiamo idealisti e quindi lo trattiamo erroneamente come l'unico possibile approccio giusto in assoluto. Pochissime app riescono ad avvicinarsi al 100% senza effetti collaterali per il semplice motivo che non farebbero nulla di utile se fossero veramente privi di effetti collaterali. Abbiamo bisogno di cambiare stato, abbiamo bisogno di IO ecc. Affinché l'app sia utile. E in questi casi, i metodi devono causare effetti collaterali e quindi non possono essere puri. Ma la regola empirica qui è di ridurre al minimo questi metodi "impuri"; hanno solo effetti collaterali perché ne hanno bisogno, piuttosto che come norma.

Mi sembra ragionevole avere uno stato di battaglia. Dato che si tratta di un gioco a turni, tengo gli stati di battaglia in un dizionario (multiplayer - potrebbero esserci molte battaglie giocate da molti giocatori contemporaneamente). Ogni volta che un giocatore fa il suo turno, chiamo un metodo appropriato sullo stato di battaglia che (a) modifica lo stato di conseguenza e (b) restituisce gli aggiornamenti ai giocatori, che vengono serializzati in JSON e sostanzialmente dicono loro cosa è appena successo sul tavola.

Mi sembra più che ragionevole avere uno stato di battaglia; sembra essenziale. Lo scopo di tale codice è gestire le richieste di cambio di stato, gestire tali cambiamenti di stato e riportarli indietro. Potresti gestire quello stato a livello globale, puoi tenerlo all'interno dei singoli oggetti del giocatore o puoi passarlo attorno a una serie di funzioni pure. Quello che scegli scende a quale funziona meglio per il tuo particolare scenario. Lo stato globale semplifica la progettazione del codice ed è veloce, che è un requisito fondamentale per la maggior parte dei giochi. Ma rende il codice molto più difficile da mantenere, testare ed eseguire il debug. Una serie di funzioni pure renderà il codice più complesso da implementare e rischia di essere troppo lento a causa dell'eccessiva copia dei dati. Ma sarà il più semplice da testare e mantenere. L '"approccio OO" si colloca a metà strada.

La chiave è: non esiste una soluzione perfetta che funzioni sempre. Lo scopo delle funzioni pure è di aiutarti a "cadere nella fossa del successo". Ma se quella fossa è così superficiale, a causa della complessità che può portare al codice, che non ci cadi così tanto come inciampare su di essa, allora non è l'approccio giusto per te. Punta all'ideale, ma sii pragmatico e fermati quando quell'ideale non è un buon posto dove andare questa volta.

E come ultimo punto, solo per ribadire: le funzioni pure e "dire, non chiedere" non sono affatto opposti.


5

Per qualsiasi cosa, mai detto, esiste un contesto, in cui puoi mettere quell'affermazione, che la renderà assurda.

inserisci qui la descrizione dell'immagine

Lo zio Bob ha completamente torto se prendi il consiglio argomento zero come requisito. Ha perfettamente ragione se prendi questo per significare che ogni argomento aggiuntivo rende il codice più difficile da leggere. Ha un costo. Non aggiungi argomenti alle funzioni perché le rendono più facili da leggere. Aggiungi argomenti alle funzioni perché non riesci a pensare a un buon nome che renda evidente la dipendenza da quell'argomento.

Ad esempio pi()è una funzione perfettamente funzionante così com'è. Perché? Perché non mi interessa come, o anche se, è stato calcolato. O se ha usato e, o sin (), per arrivare al numero che restituisce. Sto bene perché il nome mi dice tutto quello che devo sapere.

Tuttavia, non tutti i nomi mi dicono tutto ciò che devo sapere. Alcuni nomi non rivelano importanti per comprendere le informazioni che controllano il comportamento della funzione così come fanno gli argomenti esposti. Proprio così c'è ciò che rende più semplice ragionare sullo stile funzionale della programmazione.

Posso mantenere le cose immutabili e senza effetti collaterali in uno stile completamente OOP. Il ritorno è semplicemente un meccanico utilizzato per lasciare valori nello stack per la procedura successiva. Puoi rimanere altrettanto immutabile usando le porte di output per comunicare valori ad altre cose immutabili fino a quando non colpisci l'ultima porta di output che alla fine deve cambiare qualcosa se vuoi che le persone siano in grado di leggerlo. Questo è vero per ogni lingua, funzionale o no.

Quindi, per favore, non pretendere che la programmazione funzionale e la programmazione orientata agli oggetti siano "fondamentalmente incompatibili". Posso usare oggetti nei miei programmi funzionali e posso usare funzioni pure nei miei programmi OO.

Tuttavia, c'è un costo per mescolarli: aspettative. Puoi seguire fedelmente la meccanica di entrambi i paradigmi e comunque creare confusione. Uno dei vantaggi dell'utilizzo di un linguaggio funzionale è che gli effetti collaterali, sebbene debbano esistere per ottenere qualsiasi risultato, sono collocati in un posto prevedibile. A meno che ovviamente un oggetto mutevole non sia accessibile in modo indisciplinato. Quindi ciò che hai preso come un dato in quella lingua cade a pezzi.

Allo stesso modo, puoi supportare oggetti con funzioni pure, puoi progettare oggetti immutabili. Il problema è che se non segnali che le funzioni sono pure o che gli oggetti sono immutabili, le persone non traggono alcun vantaggio dal ragionamento fino a quando non trascorrono molto tempo a leggere il codice.

Questo non è un nuovo problema. Per anni le persone hanno codificato proceduralmente in "lingue OO" pensando di fare OO perché usano una "lingua OO". Poche lingue sono così brave a impedirti di spararti ai piedi. Perché queste idee funzionino, devono vivere in te.

Entrambi forniscono buone funzionalità. Puoi fare entrambe le cose. Se sei abbastanza coraggioso da mescolarli, ti preghiamo di etichettarli chiaramente.


0

A volte faccio fatica a dare un senso a tutte le regole di vari paradigmi. A volte sono in contrasto tra loro come in questa situazione.

OOP è un paradigma imperativo che riguarda correre con le forbici nel mondo in cui accadono cose pericolose.

FP è un paradigma funzionale in cui si trova assoluta sicurezza nel calcolo puro. Qui non succede nulla.

Tuttavia, tutti i programmi devono essere collegati al mondo imperativo per essere utili. Quindi, nucleo funzionale, guscio imperativo .

Le cose diventano confuse quando si inizia a definire oggetti immutabili (quelli i cui comandi restituiscono una copia modificata anziché essere effettivamente mutati). Ti dici "Questo è OOP" e "Sto definendo il comportamento degli oggetti". Ripensi al principio provato e testato Tell, Don't Ask. Il problema è che lo stai applicando al regno sbagliato.

I regni sono completamente diversi e rispettano regole diverse. Il regno funzionale si sviluppa fino al punto in cui vuole rilasciare effetti collaterali nel mondo. Affinché tali effetti vengano rilasciati, tutti i dati che sarebbero stati incapsulati in un oggetto imperativo (se questo fosse stato scritto in questo modo!) Devono essere a disposizione della shell imperativa. Senza l'accesso a questi dati che in un altro mondo sarebbero stati nascosti tramite l'incapsulamento, non può fare il lavoro. È computazionalmente impossibile.

Pertanto, quando scrivi oggetti immutabili (ciò che Clojure chiama strutture di dati persistenti) ricorda che sei nel dominio funzionale. Getta Tell, Don't Ask fuori dalla finestra, e lascialo rientrare in casa solo quando si rientra nel regno imperativo.

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.