Quando si programma in stile funzionale, si dispone di un singolo stato dell'applicazione che si intreccia nella logica dell'applicazione?


12

Come faccio a costruire un sistema che abbia tutto quanto segue :

  1. Utilizzo di funzioni pure con oggetti immutabili.
  2. Passa in una funzione solo i dati della funzione di cui ha bisogno, non di più (ovvero nessun oggetto di stato di applicazione di grandi dimensioni)
  3. Evita di avere troppi argomenti per le funzioni.
  4. Evitare di dover costruire nuovi oggetti solo allo scopo di impacchettare e decomprimere i parametri in funzioni, semplicemente per evitare che troppi parametri vengano passati alle funzioni. Se ho intenzione di impacchettare più elementi in una funzione come un singolo oggetto, voglio che quell'oggetto sia il proprietario di quei dati, non qualcosa di costruito temporaneamente

Mi sembra che la monade di Stato infranga la regola # 2, anche se non è ovvio perché è intrecciata attraverso la monade.

Ho la sensazione che ho bisogno di usare gli obiettivi in ​​qualche modo, ma molto poco è scritto a riguardo per i linguaggi non funzionali.

sfondo

Come esercizio, sto convertendo una delle mie applicazioni esistenti da uno stile orientato agli oggetti a uno stile funzionale. La prima cosa che sto cercando di fare è di rendere il più possibile il nucleo interno dell'applicazione.

Una cosa che ho sentito è che come gestire lo "Stato" in un linguaggio puramente funzionale, e questo è ciò che credo sia fatto dalle monadi statali, è che logicamente, tu chiami una funzione pura ", passando nello stato del mondo com'è ", quindi quando la funzione ritorna, ti restituisce lo stato del mondo come è cambiato.

Per illustrare, il modo in cui puoi fare un "mondo di ciao" in un modo puramente funzionale è un po 'come, passi nel tuo programma quello stato dello schermo e ricevi indietro lo stato dello schermo con "ciao mondo" stampato su di esso. Quindi tecnicamente, stai chiamando una funzione pura e non ci sono effetti collaterali.

Sulla base di ciò, ho esaminato la mia applicazione e: 1. Prima ho messo tutto il mio stato dell'applicazione in un singolo oggetto globale (GameState) 2. In secondo luogo, ho reso GameState immutabile. Non puoi cambiarlo. Se hai bisogno di un cambiamento, devi costruirne uno nuovo. L'ho fatto aggiungendo un costruttore di copia, che opzionalmente prende uno o più campi che sono cambiati. 3. Ad ogni applicazione, passo il GameState come parametro. All'interno della funzione, dopo aver fatto quello che sta per fare, crea un nuovo GameState e lo restituisce.

Come ho un core funzionale puro e un loop all'esterno che alimenta GameState nel loop del flusso di lavoro principale dell'applicazione.

La mia domanda:

Ora, il mio problema è che GameState ha circa 15 diversi oggetti immutabili. Molte delle funzioni al livello più basso operano solo su alcuni di quegli oggetti, come tenere il punteggio. Quindi, diciamo che ho una funzione che calcola il punteggio. Oggi GameState viene passato a questa funzione, che modifica il punteggio creando un nuovo GameState con un nuovo punteggio.

Qualcosa al riguardo sembra sbagliato. La funzione non ha bisogno dell'intero GameState. Ha solo bisogno dell'oggetto Score. Quindi l'ho aggiornato per passare il punteggio e restituire solo il punteggio.

Sembrava avere senso, quindi sono andato oltre con altre funzioni. Alcune funzioni mi richiederebbero di passare 2, 3 o 4 parametri da GameState, ma poiché ho usato il modello fino in fondo al nucleo esterno dell'applicazione, sto passando sempre più lo stato dell'applicazione. Come, all'inizio del ciclo del flusso di lavoro, chiamerei un metodo, che chiamerebbe un metodo che chiamerebbe un metodo, ecc., Fino al punto in cui viene calcolato il punteggio. Ciò significa che il punteggio corrente viene passato attraverso tutti quei livelli solo perché una funzione in fondo calcolerà il punteggio.

Quindi ora ho funzioni con a volte dozzine di parametri. Potrei mettere quei parametri in un oggetto per abbassare il numero di parametri, ma poi vorrei che quella classe fosse la posizione principale dello stato dell'applicazione di stato, piuttosto che un oggetto che è semplicemente costruito al momento della chiamata semplicemente per evitare il passaggio in più parametri, quindi scompattarli.

Quindi ora mi chiedo se il problema che ho è che le mie funzioni sono nidificate troppo profondamente. Questo è il risultato del voler avere piccole funzioni, quindi rifletto quando una funzione diventa grande e la divido in più funzioni più piccole. Ma farlo produce una gerarchia più profonda e tutto ciò che passa alle funzioni interne deve essere passato alla funzione esterna anche se la funzione esterna non opera direttamente su quegli oggetti.

Sembrava semplicemente passare nel GameState lungo la strada per evitare questo problema. Ma sono tornato al problema originale di passare più informazioni a una funzione di quante ne abbia bisogno.


1
Non sono un esperto di design e specialmente funzionale, ma poiché il tuo gioco per natura ha uno stato che si evolve, sei sicuro che la programmazione funzionale sia un paradigma che si adatta a tutti i livelli della tua applicazione?
Walfrat,

Walfrat, penso che se parli con esperti di programmazione funzionale, probabilmente scoprirai che direbbero che il paradigma di programmazione funzionale ha soluzioni per gestire lo stato in evoluzione.
Daisha Lynn,

La tua domanda mi è sembrata più ampia che afferma solo. Se è solo sugli stati che gestiscono qui è un inizio: vedere la risposta e link all'interno stackoverflow.com/questions/1020653/...
Walfrat

2
@DaishaLynn Non credo che dovresti eliminare la domanda. È stato votato e nessuno sta cercando di chiuderlo, quindi non credo che sia fuori portata per questo sito. La mancanza di una risposta finora può essere solo perché richiede una competenza relativamente di nicchia. Ma ciò non significa che non verrà trovato e risposto alla fine.
Ben Aaronson,

2
Gestire lo stato mutevole in un complesso programma funzionale puro senza sostanziale assistenza linguistica è un dolore enorme. In Haskell è gestibile a causa di monadi, sintassi concisa, inferenza del tipo molto buona, ma può ancora essere molto fastidioso. In C # penso che avresti molti più problemi.
Ripristina Monica il

Risposte:


2

Non sono sicuro che ci sia una buona soluzione. Questa potrebbe essere o meno una risposta, ma è troppo lunga per un commento. Stavo facendo una cosa simile e i seguenti trucchi mi hanno aiutato:

  • Dividi GameStategerarchicamente, in modo da ottenere 3-5 parti più piccole invece di 15.
  • Lascia che implementi le interfacce, in modo che i tuoi metodi vedano solo le parti necessarie. Non rimetterli mai indietro poiché mentiresti a te stesso sul tipo reale.
  • Lascia che anche le parti implementino le interfacce, in modo da avere un controllo accurato di ciò che passi.
  • Usa gli oggetti parametro, ma fallo con parsimonia e cerca di trasformarli in oggetti reali con il loro comportamento.
  • A volte passare leggermente più del necessario è meglio di un lungo elenco di parametri.

Quindi ora mi chiedo se il problema che ho è che le mie funzioni sono nidificate troppo profondamente.

Io non la penso così. Rifattorizzare in piccole funzioni è giusto, ma forse potresti raggrupparle meglio. A volte, non è possibile, a volte ha solo bisogno di una seconda (o terza) occhiata al problema.

Confronta il tuo design con quello mutevole. Ci sono cose che sono peggiorate dalla riscrittura? In tal caso, non puoi migliorarli come facevi in ​​origine?


Qualcuno mi ha detto di cambiare il mio design in modo che mai la funzione prenda solo un parametro in modo da poter usare il curry. Ho provato quell'unica funzione, quindi invece di chiamare DeleteEntity (a, b, c), ora chiamo DeleteEntity (a) (b) (c). Quindi è carino e dovrebbe rendere le cose più compostabili, ma non lo capisco ancora.
Daisha Lynn,

@DaishaLynn Sto usando Java e non c'è zucchero dolce sintattico per il curry, quindi (per me) non vale la pena provare. Sono piuttosto scettico riguardo al possibile utilizzo delle funzioni di ordine superiore nel nostro caso, ma fatemi sapere se ha funzionato per voi.
maaartinus,

2

Non posso parlare con C #, ma a Haskell, finiresti per passare l'intero stato in giro. Potresti farlo esplicitamente o con una monade di stato. Una cosa che puoi fare per affrontare il problema delle funzioni che ricevono più informazioni di quelle di cui hanno bisogno è usare Has typeclass. (Se non si ha familiarità, le typeclass di Haskell sono un po 'come le interfacce C #). Per ogni elemento E dello stato, è possibile definire una HasE di una typeclass che richiede una funzione getE che restituisca il valore di E. La monade dello stato può quindi essere fatto un'istanza di tutti questi tipi di macchine da scrivere. Quindi, nelle tue effettive funzioni, invece di richiedere esplicitamente la tua monade di stato, richiedi qualsiasi monade che appartiene alla tabella dei caratteri Has per gli elementi di cui hai bisogno; ciò limita ciò che la funzione può fare con la monade che sta usando. Per maggiori informazioni su questo approccio, vedi Michael Snoymanpubblicare sul modello di progettazione ReaderT .

Probabilmente potresti replicare qualcosa del genere in C #, a seconda di come stai definendo lo stato che viene passato. Se hai qualcosa del genere

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

potresti definire interfacce IHasMyInte IHasMyStringcon metodi GetMyInte GetMyStringrispettivamente. La classe statale si presenta quindi come:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

quindi i tuoi metodi potrebbero richiedere IHasMyInt, IHasMyString o l'intero MyState, a seconda dei casi.

È quindi possibile utilizzare il vincolo where sulla definizione della funzione in modo da poter passare l'oggetto state, ma può arrivare solo a string e int, non al doppio.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}

Interessante. Quindi attualmente, quando passo, chiamo una funzione e passo 10 parametri in base al valore, passo 10 volte "gameSt'ate", ma a 10 diversi tipi di parametri, come "IHasGameScore", "IHasGameBoard", ecc. è stato un modo per passare passare un singolo parametro che la funzione può indicare deve implementare tutte le interfacce in quel tipo. Mi chiedo se posso farlo con un "vincolo generico" .. Fammi provare.
Daisha Lynn,

1
Ha funzionato. Qui funziona: dotnetfiddle.net/cfmDbs .
Daisha Lynn,

1

Penso che faresti bene a conoscere Redux o Elm e come gestiscono questa domanda.

Fondamentalmente, hai una pura funzione che accetta l'intero stato e l'azione eseguita dall'utente e restituisce il nuovo stato.

Quella funzione chiama quindi altre funzioni pure, ognuna delle quali gestisce un particolare pezzo dello stato. A seconda dell'azione, molte di queste funzioni potrebbero non fare altro che restituire invariato lo stato originale.

Per saperne di più, Google the Elm Architecture o Redux.js.org.


Non conosco Elm, ma credo sia simile a Redux. In Redux, non tutti i riduttori vengono chiamati per ogni cambio di stato? Sembra estremamente inefficiente.
Daisha Lynn,

Quando si tratta di ottimizzazioni di basso livello, non assumere, misurare. In pratica è abbastanza veloce.
Daniel T.

Grazie Daniel, ma non funzionerà per me. Ho fatto abbastanza sviluppo per sapere che non viene notificato ogni componente dell'interfaccia utente ogni volta che vengono modificati i dati, indipendentemente dal fatto che il controllo sia interessato al controllo.
Daisha Lynn,

-2

Penso che quello che stai cercando di fare sia usare un linguaggio orientato agli oggetti in un modo che non dovrebbe essere usato, come se fosse un puro funzionale. Non è che le lingue OO fossero tutte malvagie. Ci sono vantaggi di entrambi gli approcci, quindi è per questo che ora possiamo mescolare lo stile OO con lo stile funzionale e avere l'opportunità di rendere funzionali alcuni pezzi di codice mentre altri rimangono orientati agli oggetti in modo da poter sfruttare tutta la riusabilità, l'eredità o il polimofismo. Fortunatamente non siamo più tenuti ad avvicinarci a nessuno dei due approcci, quindi perché stai cercando di limitarti a uno di essi?

Rispondere alla tua domanda: no, non tesso alcun particolare stato attraverso la logica dell'applicazione ma uso ciò che è appropriato per il caso d'uso corrente e applico le tecniche disponibili nel modo più appropriato.

C # non è pronto (ancora) per essere utilizzato come funzionale come vorresti che fosse.


3
Non entusiasta di questa risposta, né del tono. Non sto abusando di nulla. Sto spingendo i limiti di C # per usarlo come linguaggio più funzionale. Non è una cosa insolita da fare. Sembri filosoficamente contrario a ciò, il che va bene, ma in quel caso, non guardare a questa domanda. Il tuo commento non serve a nessuno. Vai avanti.
Daisha Lynn,

@DaishaLynn ti sbagli, non mi oppongo in alcun modo ad esso e in effetti lo uso molto ... ma dove è naturale e possibile e non provo a trasformare un linguaggio OO in un linguaggio funzionale solo perché è alla moda fare così. Non devi essere d'accordo con la mia risposta, ma ciò non cambia il fatto che non stai utilizzando correttamente i tuoi strumenti.
t3chb0t

Non lo sto facendo perché è alla moda farlo. C # stesso si sta muovendo verso l'utilizzo di uno stile funzionale. Lo stesso Anders Hejlsberg lo ha indicato come tale. Comprendo che sei interessato solo all'utilizzo del flusso principale della lingua e capisco perché e quando è appropriato. Semplicemente non so perché qualcuno come te sia anche su questo thread .. Come stai aiutando davvero?
Daisha Lynn,

@DaishaLynn se non riesci a far fronte alle risposte che criticano la tua domanda o il tuo approccio, probabilmente non dovresti fare domande qui o la prossima volta dovresti semplicemente aggiungere un disclaimer che dice che sei interessato solo a risposte a sostegno della tua idea al 100% perché non lo fai voglio sentire la verità, ma piuttosto ottenere opinioni di supporto.
t3chb0t

Si prega di essere un po 'più cordiali l'uno con l'altro. È possibile dare una critica senza denigrare il linguaggio. Tentare di programmare C # in uno stile funzionale non è certamente un "abuso" o un caso limite. È una tecnica comune utilizzata da molti sviluppatori C # per imparare da altre lingue.
zumalifeguard,
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.