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

12

Come faccio a costruire un sistema che ha tutti i seguenti :

  1. Uso di funzioni pure con oggetti immutabili
  2. Passa solo in una funzione dati che la funzione di cui ha bisogno, non più (cioè nessun grande oggetto stato dell'applicazione)
  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 impaccherò più oggetti in una funzione come un singolo oggetto, voglio che quell'oggetto sia il proprietario di quei dati, non qualcosa costruito temporaneamente

Mi sembra che la monade di Stato infrange la regola n. 2, sebbene non sia ovvia perché è intrecciata attraverso la monade.

Ho la sensazione che ho bisogno di usare gli obiettivi in qualche modo, ma ne ho scritto pochissimo 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 è rendere il più possibile l'inner-core dell'applicazione.

Una cosa che ho sentito è che come gestire lo "stato" in un linguaggio puramente funzionale, e questo è quello che credo sia fatto dalle monadi di stato, è che logicamente, si chiama una funzione pura, "passando nel stato del mondo così com'è ", quindi quando la funzione ritorna, restituisce a te lo stato del mondo come è cambiato.

Per illustrare, il modo in cui si può fare un "ciao mondo" in modo puramente funzionale è un po 'come, si passa al programma lo stato dello schermo e si riceve lo stato dello schermo con "Ciao mondo" stampato su di esso. Quindi, tecnicamente, stai facendo una chiamata a una funzione pura, e non ci sono effetti collaterali.

In base a ciò, ho esaminato la mia domanda e: 1. In primo luogo, metti tutti i miei stati di 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. Ho fatto questo aggiungendo un costruttore di copia, che opzionalmente accetta uno o più campi che sono cambiati. 3. Per ogni applicazione, passo in 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 quel GameState nel loop del flusso di lavoro principale dell'applicazione.

La mia domanda:

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

Qualcosa a riguardo sembra sbagliato. La funzione non ha bisogno della totalità di GameState. Ha solo bisogno dell'oggetto Punteggio. Quindi l'ho aggiornato per passare il punteggio e restituire solo il punteggio.

Sembrava aver senso, quindi sono andato oltre con altre funzioni. Alcune funzioni mi richiederebbero di passare in 2, 3 o 4 parametri dal GameState, ma dal momento che ho usato il pattern fino in fondo al nucleo esterno dell'applicazione, sto passando in sempre più dello stato dell'applicazione. Ad esempio, 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 lungo tutti questi livelli solo perché una funzione in fondo sta per calcolare il punteggio.

Quindi ora ho delle funzioni con qualche dozzina 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 stato, piuttosto che un oggetto che è semplicemente costruito al momento della chiamata semplicemente per evitare di passare in più parametri, quindi decomprimili.

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 mi rifatto quando una funzione diventa grande e la diviso in più funzioni più piccole. Ma fare ciò produce una gerarchia più profonda, e tutto ciò che è passato nelle funzioni interne deve essere passato alla funzione esterna anche se la funzione esterna non sta operando direttamente su quegli oggetti.

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

    
posta Daisha Lynn 05.07.2017 - 05:07
fonte

4 risposte

2

Non sono sicuro che ci sia una buona soluzione. Questo può o non essere una risposta, ma è troppo lungo per un commento. Stavo facendo una cosa simile e i seguenti trucchi mi hanno aiutato:

  • Dividi il GameState gerarchicamente, quindi ottieni 3-5 parti più piccole invece di 15.
  • Lascia che implementi le interfacce, quindi i tuoi metodi vedono solo le parti necessarie. Non gettarli mai indietro come se stessi mentendo a te stesso sul tipo reale.
  • Lascia che anche le parti implementino le interfacce, in modo da ottenere un controllo preciso su ciò che passi.
  • Usa gli oggetti parametro, ma fallo con parsimonia e prova a trasformarli in oggetti reali con il loro comportamento.
  • A volte passare un po 'più del necessario è meglio di un lungo elenco di parametri.

So now I'm wondering if the problem I have is that my functions are nested too deeply.

Io non la penso così Refactoring in piccole funzioni è giusto, ma forse potresti raggrupparle meglio. A volte, non è possibile, a volte serve solo un secondo (o terzo) esame del problema.

Confronta il tuo design con quello mutabile. Ci sono cose che sono peggiorate dalla riscrittura? Se è così, non puoi migliorarli nello stesso modo in cui sei stato originariamente?

    
risposta data 06.07.2017 - 15:20
fonte
2

Non riesco a parlare con C #, ma in Haskell, finiresti per passare l'intero stato. Potresti farlo in modo esplicito o con una monade di stato. Una cosa che puoi fare per risolvere il problema delle funzioni che ricevono più informazioni di cui hanno bisogno è usare Has typeclasses. (Se non sei familiare, i typeclass Haskell sono un po 'come le interfacce C #). Per ogni elemento E dello stato, puoi definire un HasE typeclass che richiede una funzione getE che restituisce il valore di E. La monade di stato può quindi essere fatto un'istanza di tutti questi esemplari tipografici. Quindi, nelle tue effettive funzioni, invece di richiedere esplicitamente la tua monade di stato, hai bisogno di qualsiasi monade appartenente ai typeclass Has per gli elementi di cui hai bisogno; che limita ciò che la funzione può fare con la monade che sta usando. Per maggiori informazioni su questo approccio, consulta il post di Michael Snoyman sul modello di progettazione di ReaderT .

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

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

potresti definire le interfacce IHasMyInt e IHasMyString con metodi GetMyInt e GetMyString rispettivamente. La classe di stato sembra quindi:

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;
    }
}

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

Puoi quindi utilizzare il vincolo where sulla definizione della funzione in modo che tu possa passare l'oggetto state, ma può solo arrivare a string e int, non double.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
    
risposta data 06.07.2017 - 19:41
fonte
1

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

Fondamentalmente, hai una funzione pura che prende 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 non possono fare altro che restituire lo stato originale invariato.

Per ulteriori informazioni, Google Elm Architecture o Redux.js.org.

    
risposta data 06.07.2017 - 13:34
fonte
-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 strumento funzionale. Non è come se le lingue OO fossero tutto il male. Ci sono i vantaggi di entrambi gli approcci, ecco perché ora possiamo combinare lo stile OO con lo stile funzionale e avere l'opportunità di rendere alcuni pezzi di codice funzionali mentre altri rimangono orientati agli oggetti in modo da poter sfruttare tutta la riusabilità, l'ereditarietà o il polimorfismo. Fortunatamente non siamo più legati ad alcun approccio, quindi perché stai cercando di limitarti a uno di loro?

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

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

    
risposta data 07.07.2017 - 19:49
fonte

Leggi altre domande sui tag