Sto leggendo e sentendo che le persone (anche su questo sito) lodano regolarmente il paradigma della programmazione funzionale, sottolineando quanto sia bello avere tutto immutabile. In particolare, le persone propongono questo approccio anche in linguaggi OO tradizionalmente imperativi, come C #, Java o C ++, non solo in linguaggi puramente funzionali come Haskell che impongono questo al programmatore.
Trovo difficile da capire, perché trovo la mutabilità e gli effetti collaterali ... convenienti. Tuttavia, dato come le persone attualmente condannano gli effetti collaterali e lo considerano una buona pratica per sbarazzarsene ove possibile, credo che se voglio essere un programmatore competente devo iniziare il mio percorso verso una migliore comprensione del paradigma ... Da qui il mio Q.
Un posto in cui trovo problemi con il paradigma funzionale è quando un oggetto viene naturalmente referenziato da più punti. Lascia che lo descriva su due esempi.
Il primo esempio sarà il mio gioco C # che sto cercando di realizzare nel mio tempo libero . È un gioco a turni in cui entrambi i giocatori hanno squadre di 4 mostri e possono inviare un mostro della loro squadra sul campo di battaglia, dove affronterà il mostro inviato dal giocatore avversario. I giocatori possono anche richiamare mostri dal campo di battaglia e sostituirli con un altro mostro della loro squadra (in modo simile a Pokemon).
In questa impostazione, un singolo mostro può essere referenziato naturalmente da almeno 2 posti: la squadra di un giocatore e il campo di battaglia, che fa riferimento a due mostri "attivi".
Ora consideriamo la situazione in cui un mostro viene colpito e perde 20 punti salute. Tra le parentesi del paradigma imperativo health
modifico il campo di questo mostro per riflettere questo cambiamento - e questo è quello che sto facendo ora. Tuttavia, ciò rende la Monster
classe mutabile e le relative funzioni (metodi) impure, che a mio avviso sono considerate una cattiva pratica fin d'ora.
Anche se mi sono concesso il permesso di avere il codice di questo gioco in uno stato inferiore all'ideale al fine di sperare di poterlo effettivamente finire in un certo punto del futuro, vorrei sapere e capire come dovrebbe essere scritto correttamente. Pertanto: se si tratta di un difetto di progettazione, come risolverlo?
Nello stile funzionale, a quanto ho capito, farei invece una copia di questo Monster
oggetto, mantenendolo identico a quello vecchio ad eccezione di questo campo; e il metodo suffer_hit
restituirebbe questo nuovo oggetto invece di modificare quello vecchio in atto. Quindi copierei anche l' Battlefield
oggetto, mantenendo tutti i suoi campi uguali tranne questo mostro.
Questo comporta almeno 2 difficoltà:
- La gerarchia può facilmente essere molto più profonda di questo esempio semplificato di solo
Battlefield
->Monster
. Dovrei fare una tale copia di tutti i campi tranne uno e restituire un nuovo oggetto su questa gerarchia. Questo sarebbe il codice del boilerplate che trovo fastidioso soprattutto perché la programmazione funzionale dovrebbe ridurre il plateplate. - Un problema molto più grave, tuttavia, è che ciò porterebbe alla sincronizzazione dei dati . Il mostro attivo del campo vedrebbe ridotta la sua salute; tuttavia, questo stesso mostro, a cui fa riferimento il suo giocatore di controllo
Team
, non lo farebbe. Se invece abbracciassi lo stile imperativo, ogni modifica dei dati sarebbe immediatamente visibile da tutti gli altri luoghi di codice e in casi come questo lo trovo davvero conveniente - ma il modo in cui ottengo le cose è esattamente quello che la gente dice sbagliato con lo stile imperativo!- Ora sarebbe possibile occuparsi di questo problema facendo un viaggio verso il
Team
dopo ogni attacco. Questo è un lavoro extra. Tuttavia, cosa succede se un mostro può improvvisamente essere referenziato in seguito da ancora più luoghi? E se venissi con un'abilità che, ad esempio, consente a un mostro di concentrarsi su un altro mostro che non è necessariamente sul campo (sto davvero prendendo in considerazione tale abilità)? Avrò sicuramente ricordare di prendere anche un viaggio ai mostri focalizzate immediatelly dopo ogni attacco? Questa sembra essere una bomba a tempo che esploderà man mano che il codice diventa più complesso, quindi penso che non sia una soluzione.
- Ora sarebbe possibile occuparsi di questo problema facendo un viaggio verso il
Un'idea per una soluzione migliore viene dal mio secondo esempio, quando ho riscontrato lo stesso problema. Nell'accademia ci è stato detto di scrivere un interprete di una lingua di nostra progettazione in Haskell. (Questo è anche il modo in cui sono stato costretto a iniziare a capire cos'è FP). Il problema si presentava durante l'implementazione delle chiusure. Ancora una volta è possibile fare riferimento allo stesso ambito da più posizioni: tramite la variabile che contiene questo ambito e come ambito padre di tutti gli ambiti nidificati! Ovviamente, se viene apportata una modifica a questo ambito tramite uno dei riferimenti che lo puntano, questa modifica deve essere visibile anche attraverso tutti gli altri riferimenti.
La soluzione che ho fornito è stata assegnare un ID a ciascun ambito e contenere un dizionario centrale di tutti gli ambiti nella State
monade. Ora le variabili conserverebbero solo l'ID dell'ambito a cui erano vincolate, anziché l'ambito stesso, e gli ambiti nidificati conterrebbero anche l'ID del loro ambito padre.
Immagino che lo stesso approccio potrebbe essere tentato nel mio gioco di battaglie tra mostri ... Campi e squadre non fanno riferimento ai mostri; contengono invece ID di mostri che vengono salvati in un dizionario di mostri centrale.
Tuttavia, posso ancora una volta vedere un problema con questo approccio che mi impedisce di accettarlo senza esitazione come soluzione al problema:
Ancora una volta è una fonte di codice boilerplate. Rende necessariamente una linea singola a 3 linee: ciò che prima era una modifica sul posto di una riga di un singolo campo ora richiede (a) Recuperare l'oggetto dal dizionario centrale (b) Apportare la modifica (c) Salvare il nuovo oggetto al dizionario centrale. Inoltre, contenere ID di oggetti e dizionari centrali invece di avere riferimenti aumenta la complessità. Dal momento che FP è pubblicizzato per ridurre la complessità e il codice boilerplate, questo suggerisce che sto sbagliando.
Stavo anche scrivendo del secondo problema che sembra molto più grave: questo approccio introduce perdite di memoria . Gli oggetti che non sono raggiungibili verranno normalmente raccolti. Tuttavia, gli oggetti contenuti in un dizionario centrale non possono essere garbage collection, anche se nessun oggetto raggiungibile fa riferimento a questo particolare ID. E mentre una programmazione teoricamente accurata può evitare perdite di memoria (potremmo avere cura di rimuovere manualmente ogni oggetto dal dizionario centrale quando non è più necessario), questo è soggetto a errori e FP è pubblicizzato per aumentare la correttezza dei programmi, quindi ancora una volta non essere il modo corretto.
Tuttavia, ho scoperto in tempo che sembra piuttosto essere un problema risolto. Java fornisce WeakHashMap
che potrebbe essere utilizzato per risolvere questo problema. C # offre una funzione simile - ConditionalWeakTable
- sebbene secondo i documenti sia destinata ai compilatori. E in Haskell abbiamo System.Mem.Weak .
Memorizzare tali dizionari è la soluzione funzionale corretta a questo problema o ce n'è uno più semplice che non riesco a vedere? Immagino che il numero di tali dizionari possa crescere facilmente e male; quindi se si suppone che anche questi dizionari siano immutabili, ciò può significare molto passaggio di parametri o, in lingue che lo supportano, calcoli monadici, dal momento che i dizionari sarebbero tenuti in monadi (ma ancora una volta lo sto leggendo in modo puramente funzionale le lingue con il minor codice possibile dovrebbero essere monadiche, mentre questa soluzione del dizionario collocherebbe quasi tutto il codice all'interno della State
monade; il che ancora una volta mi fa dubitare che questa sia la soluzione corretta.)
Dopo alcune considerazioni penso che aggiungerei un'altra domanda: cosa stiamo guadagnando costruendo tali dizionari? Che ciò che non va nella programmazione imperativa è, secondo molti esperti, che i cambiamenti in alcuni oggetti si propagano ad altri pezzi di codice. Per risolvere questo problema, si suppone che gli oggetti siano immutabili, proprio per questo motivo, se ho capito bene, che le modifiche apportate non dovrebbero essere visibili altrove. Ma ora sono preoccupato per altri pezzi di codice che operano su dati non aggiornati, quindi invento dizionari centrali in modo che ... ancora una volta le modifiche in alcuni pezzi di codice si propaghino ad altri pezzi di codice! Non siamo dunque tornati allo stile imperativo con tutti i suoi presunti inconvenienti, ma con maggiore complessità?
Team
) possono recuperare l'esito della battaglia e quindi gli stati dei mostri mediante una tupla (numero di battaglia, ID entità mostro).