cosa può andare storto nel contesto della programmazione funzionale se il mio oggetto è mutabile?

9

Riesco a vedere i benefici degli oggetti mutevoli e immutabili come gli oggetti immutabili che richiedono molto tempo per risolvere i problemi nella programmazione multi-thread a causa dello stato condiviso e scrivibile. Al contrario, gli oggetti mutabili aiutano a gestire l'identità dell'oggetto anziché crearne di nuove ogni volta e quindi migliorano anche le prestazioni e l'utilizzo della memoria soprattutto per gli oggetti più grandi.

Una cosa che sto cercando di capire è che cosa può andare storto nell'avere oggetti mutabili nel contesto della programmazione funzionale. Come uno dei punti che mi sono detto è che il risultato di chiamare le funzioni in un ordine diverso non è deterministico.

Sto cercando un esempio concreto concreto in cui è molto evidente ciò che può andare storto usando l'oggetto mutabile nella programmazione delle funzioni. Fondamentalmente, se è male, è negativo indipendentemente dal paradigma di programmazione funzionale o funzionale, giusto?

Credo che di seguito la mia stessa affermazione risponda a questa domanda. Ma ho ancora bisogno di qualche esempio per poterlo sentire più naturalmente.

OO helps to manage dependency and write easier and maintainable program with the aid of tools like encapsulation, polymorphism etc.

Functional programming also have same motive of promoting maintainable code but by using style which eliminates the need for using OO tools and techniques - one of which I believe is by minimizing side effects, pure function etc.

    
posta Rahul Agarwal 30.04.2018 - 10:02
fonte

6 risposte

6

Penso che l'importanza sia dimostrata al meglio confrontando un approccio OO

per esempio, diciamo che abbiamo un oggetto

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

Nel paradigma OO il metodo è collegato ai dati e ha senso che i dati vengano modificati con il metodo.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

Nel Paradigma funzionale definiamo un risultato in termini di funzione. un ordine acquistato IS il risultato della funzione di acquisto applicata a un ordine. Ciò implica alcune cose di cui dobbiamo essere sicuri

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

Ti aspetti order.Status == "Acquistato"?

Implica anche che le nostre funzioni siano idempotenti. vale a dire. eseguendoli due volte dovrebbe produrre sempre lo stesso risultato.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Se l'ordine è stato modificato dalla funzione di acquisto, purchaseOrder2 fallirebbe.

Definendo le cose come risultati di funzioni ci permette di usare quei risultati senza effettivamente calcolarli. Che in termini di programmazione è un'esecuzione differita.

Questo può essere utile in sé stesso, ma una volta che non siamo sicuri di quando una funzione avverrà effettivamente E stiamo bene, possiamo sfruttare l'elaborazione parallela molto più di quanto possiamo in un paradigma OO.

Sappiamo che l'esecuzione di una funzione non influirà sui risultati di un'altra funzione; quindi possiamo lasciare il computer per eseguirli nell'ordine che preferisce, usando tutti i thread che vuole.

Se una funzione muta il suo input, dobbiamo fare molta più attenzione a queste cose.

    
risposta data 30.04.2018 - 13:07
fonte
12

La chiave per capire perché gli oggetti immutabili sono vantaggiosi non sta nel cercare di trovare esempi concreti nel codice funzionale. Poiché la maggior parte del codice funzionale è scritta utilizzando i linguaggi funzionali e la maggior parte dei linguaggi funzionali è di default immutabile, la natura stessa del paradigma è progettata per evitare quello che stai cercando, dal verificarsi.

La cosa fondamentale da chiedere è, qual è il vantaggio dell'immutabilità? La risposta è, evita la complessità. Supponiamo di avere due variabili, x e y . Entrambi iniziano con il valore di 1 . y sebbene raddoppia ogni 13 secondi. Quale sarà il valore di ognuno di essi tra 20 giorni? x sarà 1 . Questo è facile. Ci vorrebbe uno sforzo per calcolare y in quanto è molto più complesso. A che ora del giorno tra 20 giorni? Devo tenere conto dell'ora legale? La complessità di y contro x è solo molto di più.

E ciò si verifica anche nel codice reale. Ogni volta che aggiungi un valore di mutazione al mix, questo diventa un altro valore complesso da tenere e calcolare nella tua testa, o su carta, quando cerchi di scrivere, leggere o eseguire il debug del codice. Maggiore è la complessità, maggiore è la possibilità che tu commetta un errore e che introduca un bug. Il codice è difficile da scrivere; difficile da leggere; difficile da debug: il codice è difficile da ottenere.

Tuttavia, la mutabilità non è cattiva . Un programma con zero mutabilità non può avere esito, il che è abbastanza inutile. Anche se la mutabilità è scrivere un risultato su schermo, disco o qualsiasi altra cosa, deve essere presente. Ciò che è male è la complessità inutile. Uno dei modi più semplici per ridurre la complessità consiste nel rendere le cose immutabili per impostazione predefinita e renderle modificabili solo quando necessario, a causa di prestazioni o motivi funzionali.

    
risposta data 30.04.2018 - 11:21
fonte
8

what can go wrong in context of functional programming

Le stesse cose che possono andare storte nella programmazione non funzionale: puoi ottenere indesiderati effetti collaterali , che è una causa ben nota di errori dall'invenzione dei linguaggi di programmazione con ambito.

IMHO l'unica vera differenza tra la programmazione funzionale e quella non funzionale è che, nel codice non funzionale, normalmente ti aspetteresti effetti collaterali, nella programmazione funzionale, non lo farai.

Basically if it is bad, it is bad irrespective of OO or functional programming paradigm, right?

Certo - gli effetti collaterali indesiderati sono una categoria di bug, indipendentemente dal paradigma. È vero anche l'opposto: gli effetti collaterali deliberatamente utilizzati possono aiutare a gestire i problemi di prestazioni e sono in genere necessari per la maggior parte dei programmi reali quando si tratta di I / O e si occupano di sistemi esterni, indipendentemente dal paradigma.

    
risposta data 30.04.2018 - 13:24
fonte
4

Ho appena risposto a una domanda StackOverflow che illustra abbastanza bene la tua domanda. Il problema principale con strutture di dati mutabili è che la loro identità è valida solo in un preciso istante temporale, quindi le persone tendono a stipare quanto più possibile nel piccolo punto del codice in cui sanno che l'identità è costante. In questo particolare esempio, sta facendo un sacco di logging all'interno di un ciclo for:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

Quando si è abituati all'immutabilità, non c'è alcun timore che la struttura dei dati cambi se si attende troppo a lungo, quindi è possibile svolgere attività logicamente separate a proprio piacimento, in un modo molto più disaccoppiato:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
    
risposta data 30.04.2018 - 14:39
fonte
3

Il vantaggio dell'utilizzo di oggetti immutabili è che se si riceve un riferimento a un oggetto con tale avrà una certa proprietà quando il ricevitore lo esamina, e ha bisogno di dare qualche altro codice un riferimento a un oggetto con quella stessa proprietà, uno può semplicemente passare il riferimento all'oggetto senza riguardo per chi altro potrebbe aver ricevuto il riferimento o cosa potrebbero fare all'oggetto [poiché non c'è niente che nessun altro possa fare all'oggetto], o quando il il ricevitore potrebbe esaminare l'oggetto [poiché tutte le sue proprietà saranno le stesse indipendentemente da quando vengono esaminate].

Per contrasto, il codice che deve dare a qualcuno un riferimento a un oggetto mutabile che avrà una certa proprietà quando il ricevitore lo esamina (supponendo che il ricevitore stesso non lo cambi) deve sapere che nient'altro che il ricevitore cambierà mai quella proprietà, o saprà quando il ricevitore accederà a quella proprietà e saprà che nulla cambierà la proprietà fino all'ultima volta che il ricevitore la esaminerà.

Penso che sia di grande aiuto, per la programmazione in generale (non solo la programmazione funzionale) pensare a oggetti immutabili come se fossero suddivisi in tre categorie:

  1. Gli oggetti che non possono non permetteranno nulla di cambiarli, anche con un riferimento. Tali oggetti e riferimenti ad essi si comportano come valori e possono essere liberamente condivisi.

  2. Oggetti che potrebbero essere modificati dal codice che ha dei riferimenti, ma i cui riferimenti non saranno mai esposti a nessun codice che effettivamente modifichi. Questi oggetti incapsulano valori, ma possono essere condivisi solo con codice di cui è possibile attendersi di non modificarli o esporli a codice che potrebbe fare.

  3. Oggetti che verranno modificati. Questi oggetti sono meglio visualizzati come contenitori e fanno riferimento ad essi come identificatori .

Un pattern utile è spesso quello di avere un oggetto per creare un contenitore, popolarlo usando un codice che può essere considerato attendibile per non conservare un riferimento in seguito, e quindi avere i soli riferimenti che saranno sempre presenti nell'universo nel codice che sarà non modificare mai l'oggetto una volta popolato. Mentre il contenitore potrebbe essere di un tipo mutevole, può essere ragionato su (*) come se fosse immutabile, dal momento che nulla potrà mai mutarlo. Se tutti i riferimenti al contenitore sono mantenuti in tipi di wrapper immutabili che non altereranno mai il suo contenuto, tali wrapper possono essere passati in sicurezza come se i dati all'interno di essi fossero contenuti in oggetti immutabili, poiché i riferimenti ai wrapper possono essere liberamente condivisi ed esaminati a in qualsiasi momento.

(*) Nel codice multi-thread, potrebbe essere necessario usare "barriere di memoria" per garantire che prima che qualsiasi thread possa vedere qualsiasi riferimento al wrapper, gli effetti di tutte le azioni sul contenitore sarebbero visibili a quello thread, ma questo è un caso speciale menzionato qui solo per completezza.

    
risposta data 30.04.2018 - 18:13
fonte
1

Come è già stato menzionato, il problema dello stato mutabile è fondamentalmente una sottoclasse del più ampio problema degli effetti collaterali , dove il tipo di ritorno di una funzione non descrive esattamente ciò che la funzione fa realmente, perché in questo caso, fa anche la mutazione di stato. Questo problema è stato risolto da alcuni nuovi linguaggi di ricerca, come F * ( link ). Questo linguaggio crea un sistema di effetti simile al sistema di tipi, in cui una funzione non solo dichiara staticamente il suo tipo, ma anche i suoi effetti. In questo modo, i chiamanti della funzione sono consapevoli che può verificarsi una mutazione di stato quando si chiama la funzione e tale effetto viene propagato ai chiamanti.

    
risposta data 30.04.2018 - 15:11
fonte