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


9

Riesco a vedere i vantaggi di oggetti mutabili vs immutabili come gli oggetti immutabili che tolgono molto dalla risoluzione dei problemi nella programmazione multi-thread a causa dello stato condiviso e scrivibile. Al contrario, gli oggetti mutabili aiutano a gestire l'identità dell'oggetto piuttosto che creare ogni volta una nuova copia e quindi migliorare anche le prestazioni e l'utilizzo della memoria soprattutto per oggetti più grandi.

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

Sto cercando un vero esempio concreto in cui è molto evidente cosa può andare storto usando l'oggetto mutabile nella programmazione delle funzioni. Fondamentalmente se è male, è cattivo indipendentemente da OO o paradigma di programmazione funzionale, giusto?

Credo che di seguito la mia stessa affermazione risponda a questa domanda. Ma ho ancora bisogno di qualche esempio per sentirlo in modo più naturale.

OO aiuta a gestire la dipendenza e scrivere programmi più facili e mantenibili con l'aiuto di strumenti come incapsulamento, polimorfismo ecc.

La programmazione funzionale ha anche lo stesso motivo di promuovere il codice gestibile, ma usando lo stile che elimina la necessità di utilizzare strumenti e tecniche OO - uno dei quali credo sia minimizzare gli effetti collaterali, la pura funzione ecc.


1
@Ruben direi che la maggior parte dei linguaggi funzionali consente variabili mutabili, ma rende diverso usarli, ad esempio le variabili mutabili hanno un tipo diverso
jk.

1
Penso che potresti aver mescolato immutabile e mutevole nel tuo primo paragrafo?
jk.

1
@jk., lo ha sicuramente fatto. Modificato per correggerlo.
David Arno,

6
@Ruben La programmazione funzionale è un paradigma. Pertanto, non richiede un linguaggio di programmazione funzionale. E alcuni linguaggi FP come F # hanno questa funzione .
Christophe,

1
@Ruben no in particolare stavo pensando a Mvars in haskell hackage.haskell.org/package/base-4.9.1.0/docs/… ovviamente lingue diverse hanno soluzioni diverse o IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html anche se ovviamente useresti entrambi dall'interno delle monadi
jk.

Risposte:


7

Penso che l'importanza sia dimostrata meglio confrontandola con un approccio OO

ad esempio, supponiamo di avere un oggetto

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

Nel paradigma OO il metodo è collegato ai dati e ha senso che tali dati vengano mutati dal 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 È 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 aspetteresti order.Status == "Acquistato"?

Implica anche che le nostre funzioni siano idempotenti. vale a dire. eseguirli due volte dovrebbe produrre lo stesso risultato ogni volta.

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 ,cquistato2 non riuscirebbe.

Definendo le cose come risultati di funzioni ci consente di utilizzare tali risultati senza calcolarli effettivamente. Che in termini di programmazione è l'esecuzione differita.

Questo può essere utile di per sé, ma una volta che non siamo sicuri di quando effettivamente accadrà una funzione E stiamo bene su questo, 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; così possiamo lasciare il computer per eseguirli nell'ordine che preferisce, usando tutti i thread che preferisce.

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


Grazie !! molto utile. Quindi la nuova implementazione dell'acquisto sarebbe simile Order Purchase() { return new Order(Status = "Purchased") } al campo di sola lettura. ? Ancora una volta perché questa pratica è più rilevante nel contesto del paradigma di programmazione delle funzioni? I vantaggi che hai citato possono essere visti anche nella programmazione OO, giusto?
rahulaga_dev,

in OO ti aspetteresti che object.Purchase () modifichi l'oggetto. Potresti renderlo immutabile, ma allora perché non passare a un paradigma funzionale completo
Ewan

Penso che il problema debba essere visualizzato perché sono un puro sviluppatore c # che è orientato agli oggetti per natura. Quindi, ciò che dici in un linguaggio che abbraccia la programmazione funzionale non richiederà la funzione "Purchase ()" che restituisce l'ordine acquistato da allegare a qualsiasi classe o oggetto, giusto?
rahulaga_dev,

3
puoi scrivere funzionale c # cambiare il tuo oggetto in una struttura, renderlo immutabile e scrivere un Func <Ordine, Ordine> Acquisto
Ewan

12

La chiave per capire perché gli oggetti immutabili sono utili non sta proprio nel cercare di trovare esempi concreti nel codice funzionale. Poiché la maggior parte del codice funzionale è scritta usando linguaggi funzionali e la maggior parte dei linguaggi funzionali sono immutabili per impostazione predefinita, la natura stessa del paradigma è progettata per evitare che ciò che stai cercando accada.

La cosa fondamentale da chiedere è: qual è il vantaggio dell'immutabilità? La risposta è, evita la complessità. Supponiamo di avere due variabili xe y. Entrambi iniziano con il valore di 1. ysebbene raddoppi ogni 13 secondi. Quale sarà il valore di ciascuno di essi tra 20 giorni? xsarà 1. Questo è facile. Ci vorrebbe uno sforzo per risolverlo yperché è molto più complesso. A che ora del giorno tra 20 giorni? Devo prendere in considerazione l'ora legale? La complessità di yversus xè solo molto di più.

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

La mutabilità non è male però. Un programma con zero mutabilità non può avere esito, il che è piuttosto inutile. Anche se la mutabilità deve scrivere un risultato sullo schermo, sul disco o altro, deve essere lì. Ciò che è male è inutile complessità. Uno dei modi più semplici per ridurre la complessità è rendere le cose immutabili per impostazione predefinita e renderle mutabili solo quando necessario, per motivi di prestazioni o funzionali.


4
"Uno dei modi più semplici per ridurre la complessità è rendere le cose immutabili di default e renderle mutabili solo quando necessario": Riepilogo molto piacevole e conciso.
Giorgio,

2
@DavidArno La complessità che descrivi rende il codice difficile da ragionare. Lo hai anche toccato quando hai detto "Il codice è difficile da scrivere; difficile da leggere; difficile da eseguire il debug; ...". Mi piacciono gli oggetti immutabili perché rendono il codice molto più facile da ragionare, non solo da solo, ma osservatori che guardano senza conoscere l'intero progetto.
disassemble-number-5

1
@RahulAgarwal, " Ma perché questo problema diventa più evidente nel contesto della programmazione funzionale ". Non Penso che forse sono confuso da quello che mi stai chiedendo in quanto il problema è molto meno evidente in FP poiché FP incoraggia l'immutabilità evitando così il problema.
David Arno,

1
@djechlin, " Come può il tuo esempio di 13 secondi diventare più facile da analizzare con un codice immutabile? " Non può: ydeve mutare; questo è un requisito. A volte dobbiamo avere un codice complesso per soddisfare requisiti complessi. Il punto che stavo cercando di sottolineare è che la complessità non necessaria dovrebbe essere evitata. I valori mutanti sono intrinsecamente più complessi di quelli fissi, quindi - per evitare inutili complessità - mutare i valori solo quando è necessario.
David Arno,

3
La mutabilità crea una crisi di identità. La tua variabile non ha più un'unica identità. Invece, la sua identità ora dipende dal tempo. Quindi simbolicamente, invece di una singola x, ora abbiamo una famiglia x_t. Qualsiasi codice che utilizza quella variabile ora dovrà preoccuparsi anche del tempo, causando ulteriore complessità menzionata nella risposta.
Alex Vong,

8

cosa può andare storto nel contesto della programmazione funzionale

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

L'unica vera differenza su questo tra programmazione funzionale e non funzionale è che, nel codice non funzionale, ti aspetteresti generalmente effetti collaterali, nella programmazione funzionale, non lo farai.

Fondamentalmente se è male, è cattivo indipendentemente da OO o paradigma di programmazione funzionale, giusto?

Certo, gli effetti collaterali indesiderati sono una categoria di bug, indipendentemente dal paradigma. È vero anche il contrario: gli effetti collaterali deliberatamente utilizzati possono aiutare a gestire i problemi di prestazioni e sono in genere necessari per la maggior parte dei programmi del mondo reale quando si tratta di I / O e di gestione di sistemi esterni, anche indipendentemente dal paradigma.


4

Ho appena risposto a una domanda StackOverflow che illustra abbastanza bene la tua domanda. Il problema principale con le strutture di dati mutabili è che la loro identità è valida solo in un preciso istante nel tempo, quindi le persone tendono a stiparsi il più possibile nel piccolo punto del codice in cui sanno che l'identità è costante. In questo esempio particolare, sta eseguendo molte registrazioni in 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 sei abituato all'immutabilità, non c'è timore che la struttura dei dati cambi se aspetti troppo a lungo, quindi puoi svolgere attività che sono logicamente separate a tuo 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")
}

3

Il vantaggio dell'uso di oggetti immutabili è che se si riceve un riferimento a un oggetto che avrà una certa proprietà quando il ricevitore lo esamina e deve dare ad altri codici un riferimento a un oggetto con quella stessa proprietà, si può semplicemente passare lungo 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 destinatario potrebbe esaminare l'oggetto [dal momento che tutto il suo le proprietà saranno le stesse indipendentemente da quando vengono esaminate].

Al contrario, il codice che deve fornire 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) o deve sapere che niente altro che il ricevitore cambierà mai quella proprietà, oppure sapere quando il destinatario accederà a quella proprietà e sapere che nulla cambierà quella proprietà fino all'ultima volta che il ricevitore la esaminerà.

Penso che sia di grande aiuto, per la programmazione in generale (non solo per la programmazione funzionale) pensare agli oggetti immutabili che rientrano in tre categorie:

  1. Gli oggetti che non possono permettere a 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 consentirebbero a se stessi di essere modificati da un codice che ha riferimenti ad essi, ma i cui riferimenti non saranno mai esposti a nessun codice che li cambierebbe effettivamente . Questi oggetti incapsulano valori, ma possono essere condivisi solo con codice di cui ci si può fidare per non modificarli o esporli a codice che potrebbe fare.

  3. Oggetti che verranno cambiati. Questi oggetti possono essere visualizzati come contenitori e riferimenti ad essi come identificatori .

Un modello utile è spesso quello di fare in modo che un oggetto crei un contenitore, lo compili usando un codice di cui ci si può fidare per non conservare un riferimento in seguito, e quindi gli unici riferimenti che esisteranno in qualsiasi parte dell'universo siano nel codice che non modificherà mai il oggetto una volta popolato. Sebbene il contenitore possa essere di tipo mutabile, può essere ragionato su (*) come se fosse immutabile, dal momento che nulla lo cambierà mai. Se tutti i riferimenti al contenitore sono conservati in tipi di involucri immutabili che non modificheranno mai il suo contenuto, tali involucri possono essere passati in sicurezza come se i dati al loro interno fossero conservati in oggetti immutabili, poiché i riferimenti agli involucri possono essere liberamente condivisi ed esaminati in in qualsiasi momento

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


grazie per la risposta impressionante !! Penso che probabilmente la fonte della mia confusione sia perché provengo da c # background e sto imparando "scrivere codice di stile funzionale in c #" che continua ovunque dicendo di evitare oggetti mutabili - ma penso che i linguaggi che abbracciano il paradigma della programmazione funzionale promuovano (o facciano valere) non sono sicuro se l'applicazione è corretta da usare) immutabilità.
rahulaga_dev,

@RahulAgarwal: è possibile avere riferimenti a un oggetto incapsulare un valore il cui significato non è influenzato dall'esistenza di altri riferimenti allo stesso oggetto, avere un'identità che li assocerebbe ad altri riferimenti allo stesso oggetto, o nessuno dei due. Se lo stato della parola reale cambia, allora il valore o l'identità di un oggetto associato a quello stato può essere costante, ma non entrambi - si dovrà cambiare. I $ 50.000 sono quelli che dovrebbero fare cosa.
supercat

1

Come già accennato, il problema con lo stato mutabile è fondamentalmente una sottoclasse del più grande problema degli effetti collaterali , in cui il tipo di ritorno di una funzione non descrive accuratamente ciò che la funzione fa realmente, perché in questo caso fa anche mutazione di stato. Questo problema è stato risolto da alcuni nuovi linguaggi di ricerca, come F * ( http://www.fstar-lang.org/tutorial/ ). 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 che l'effetto viene propagato ai suoi chiamanti.

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.