Programmazione funzionale: idee giuste sulla concorrenza e sullo stato?


21

I sostenitori di FP hanno affermato che la concorrenza è facile perché il loro paradigma evita lo stato mutevole. Non capisco

Immagina di creare un dungeon crawl multiplayer (un roguelike) usando FP in cui enfatizziamo funzioni pure e strutture di dati immutabili. Generiamo una prigione composta da stanze, corridoi, eroi, mostri e bottino. Il nostro mondo è effettivamente un oggetto grafico delle strutture e delle loro relazioni. Man mano che le cose cambiano, la nostra rappresentazione del mondo viene modificata per riflettere tali cambiamenti. Il nostro eroe uccide un topo, raccoglie una spada corta, ecc.

Per me il mondo (realtà attuale) porta questa idea di stato e mi manca il modo in cui FP lo supera. Mentre il nostro eroe agisce, le funzioni modificano lo stato del mondo. Sembra che ogni decisione (AI o umana) debba basarsi sullo stato del mondo come è nel presente. Dove permetteremmo la concorrenza? Non possiamo avere più processi contemporaneamente per ammodernare lo stato del mondo per timore che un processo basi i suoi risultati su uno stato scaduto. Mi sembra che tutto il controllo dovrebbe avvenire all'interno di un singolo ciclo di controllo in modo da elaborare sempre lo stato attuale rappresentato dal nostro attuale oggetto grafico del mondo.

Chiaramente ci sono situazioni perfettamente adatte alla concorrenza (ad esempio quando si elaborano attività isolate i cui stati sono indipendenti l'uno dall'altro).

Non riesco a vedere come la concorrenza sia utile nel mio esempio e questo potrebbe essere il problema. Potrei in qualche modo travisare l'affermazione.

Qualcuno può rappresentare meglio questa affermazione?


1
Ti riferisci allo stato condiviso; lo stato condiviso sarà sempre quello che è e richiederà sempre una qualche forma di sincronizzazione, la forma spesso preferita tra le persone pure di FP è STM che ti permette di trattare la memoria condivisa come memoria locale avendo un livello di astrazione su di essa che rende l'accesso transazionale così razza le condizioni vengono gestite automaticamente. Un'altra tecnica per la memoria condivisa è il passaggio di messaggi in cui invece di avere una memoria condivisa, hai la memoria locale e la conoscenza di altri attori per chiedere la loro memoria locale
Jimmy Hoffa,

1
Quindi ... ti stai chiedendo in che modo la concorrenza nello stato condiviso aiuta a gestire lo stato in un'applicazione a thread singolo? D'altra parte, il tuo esempio si presta chiaramente alla concorrenza concettualmente (un thread per ogni entità controllata dall'IA) indipendentemente dal fatto che sia implementato in quel modo. Sono confuso quello che stai chiedendo qui.
CA McCann,

1
in una parola, cerniere lampo
jk.

2
Ogni oggetto avrebbe la sua visione del mondo. Ci sarà un'eventuale coerenza . Probabilmente è anche come funzionano le cose nel nostro "mondo reale" con il collasso della funzione d'onda .
Herzmeister,

1
Potresti trovare interessanti "retrogames puramente funzionali": prog21.dadgum.com/23.html
user802500

Risposte:


15

Proverò a suggerire la risposta. Questa non è una risposta, solo un'illustrazione introduttiva. La risposta di @ jk indica la cosa reale, le cerniere.

Immagina di avere una struttura ad albero immutabile. Si desidera modificare un nodo inserendo un figlio. Di conseguenza, ottieni un albero completamente nuovo.

Ma la maggior parte del nuovo albero è esattamente uguale al vecchio albero. Un'implementazione intelligente riutilizzerebbe la maggior parte dei frammenti dell'albero, instradando i puntatori attorno al nodo alterato:

Da Wikipedia

Il libro di Okasaki è pieno di esempi come questo.

Quindi suppongo che potresti modificare ragionevolmente piccole parti del tuo mondo di gioco ogni mossa (raccogli una moneta), e in realtà cambiare solo piccole parti della struttura dei dati del tuo mondo (la cella in cui è stata raccolta la moneta). Le parti che appartengono solo a stati passati verranno raccolte in tempo.

Ciò probabilmente prende in considerazione la progettazione della struttura del mondo dei giochi di dati in modo appropriato. Sfortunatamente, non sono un esperto in queste materie. Sicuramente deve essere qualcosa di diverso da una matrice NxM che si userebbe come una struttura di dati mutabile. Probabilmente dovrebbe consistere in pezzi più piccoli (corridoi? Singole celle?) Che si puntano l'un l'altro, come fanno i nodi dell'albero.


3
+1: per indicare il libro di Okasaki. Non l'ho letto ma è nella mia lista delle cose da fare. Penso che ciò che hai rappresentato sia la soluzione corretta. In alternativa, puoi prendere in considerazione i tipi di unicità (Clean, en.wikipedia.org/wiki/Uniqueness_type ): utilizzando questo tipo di tipi puoi aggiornare in modo distruttivo gli oggetti dati mantenendo la trasparenza referenziale.
Giorgio,

C'è un vantaggio per le relazioni da definire tramite riferimento indiretto tramite chiavi o ID? Cioè, pensavo che un minor numero di tocchi effettivi di una struttura all'altra avrebbe richiesto un minor numero di modifiche alla struttura mondiale quando si verifica un cambiamento. O questa tecnica non è davvero utilizzata in FP?
Mario T. Lanza,

9

La risposta di 9000 è metà della risposta, le strutture di dati persistenti consentono di riutilizzare parti invariate.

Potresti già pensare comunque "hey e se volessi cambiare la radice dell'albero?" com'è l'esempio dato che ora significa cambiare tutti i nodi. Qui è dove le cerniere vengono in soccorso. Consentono di modificare l'elemento in uno stato attivo in O (1) e lo stato attivo può essere spostato in qualsiasi punto della struttura.

L'altro punto con le cerniere è che esiste una cerniera per quasi tutti i tipi di dati desiderati


Temo che mi ci vorrà del tempo per scavare in "cerniere" poiché sono ai margini di esplorare solo FP. Non ho esperienza con Haskell.
Mario T. Lanza,

Proverò ad aggiungere un esempio più tardi oggi
jk.

4

I programmi di stile funzionale creano molte opportunità come quella di usare la concorrenza. Ogni volta che trasformi, filtri o aggreghi una raccolta e tutto è puro o immutabile, c'è un'opportunità per velocizzare l'operazione con la concorrenza.

Ad esempio, supponi di eseguire le decisioni AI indipendentemente l'una dall'altra e in nessun ordine particolare. Non si alternano, prendono tutti una decisione contemporaneamente e poi il mondo avanza. Il codice potrebbe apparire così:

func MakeMonsterDecision curWorldState monster =
    ...
    ...
    return monsterDecision

func NextWorldState curWorldState =
    ...
    let monsterMakeDecisionForCurrentState = MakeMonsterDecision curWorldState
    let monsterDecisions = List.map monsterMakeDecisionForCurrentState activeMonsters
    ...
    return newWorldState

Hai una funzione per calcolare cosa farà un mostro dato uno stato mondiale e applicarlo a ogni mostro come parte del calcolo del prossimo stato mondiale. Questa è una cosa naturale da fare in un linguaggio funzionale e il compilatore è libero di eseguire in parallelo il passaggio "applicalo a ogni mostro".

In un linguaggio imperativo avresti più probabilità di iterare su ogni mostro, applicando i loro effetti al mondo. È solo più facile farlo in questo modo, perché non vuoi occuparti della clonazione o dell'aliasing complicato. Il compilatore non può eseguire i calcoli dei mostri in parallelo in quel caso, poiché le prime decisioni sui mostri influenzano le decisioni successive sui mostri.


Questo aiuta parecchio. Vedo come in un gioco ci sarebbe un grande vantaggio se i mostri decidessero contemporaneamente cosa faranno dopo.
Mario T. Lanza,

4

Ascoltare alcuni discorsi di Rich Hickey - questo in particolare - alleviava la mia confusione. In uno ha indicato che va bene che i processi simultanei potrebbero non avere lo stato più attuale. Avevo bisogno di sentirlo. Quello che avevo problemi a digerire era che i programmi sarebbero stati effettivamente accettabili basando le decisioni su istantanee del mondo che da allora sono state sostituite da quelle più recenti. Continuavo a chiedermi in che modo il PQ concorrenziale potesse aggirare il problema di basare le decisioni sul vecchio stato.

In un'applicazione bancaria non vorremmo mai basare una decisione su un'istantanea di stato che da allora è stata sostituita da una nuova (si è verificato un ritiro).

Tale concorrenza è facile perché il paradigma FP evita lo stato mutabile è un'affermazione tecnica che non tenta di dire nulla sui meriti logici di basare le decisioni su uno stato potenzialmente vecchio. FP alla fine modella ancora il cambiamento di stato. Non si può aggirare questo.


0

I sostenitori di FP hanno affermato che la concorrenza è facile perché il loro paradigma evita lo stato mutevole. Non capisco

Volevo rispondere a questa domanda generale come qualcuno che è un neofita funzionale ma è stato all'altezza dei miei occhi negli effetti collaterali nel corso degli anni e vorrei mitigarli, per tutti i tipi di motivi, incluso più facile (o specificamente "più sicuro, concorrenza ") soggetta a errori". Quando guardo i miei colleghi funzionali e quello che stanno facendo, l'erba sembra un po 'più verde e ha un profumo più gradevole, almeno in questo senso.

Algoritmi seriali

Detto questo, sul tuo esempio specifico, se il tuo problema è di natura seriale e B non può essere eseguito fino a quando A non è finito, concettualmente non puoi eseguire A e B in parallelo, qualunque cosa accada. Devi trovare un modo per spezzare la dipendenza dell'ordine come nella tua risposta basandoti su mosse parallele usando il vecchio stato del gioco, oppure usare una struttura di dati che consenta a parti di essa di essere modificate in modo indipendente per eliminare la dipendenza dell'ordine come proposto nelle altre risposte o qualcosa del genere. Ma ci sono sicuramente una serie di problemi di progettazione concettuale come questo in cui non puoi necessariamente semplicemente multithreading tutto così facilmente perché le cose sono immutabili. Alcune cose saranno di natura seriale fino a quando non troverai un modo intelligente per interrompere la dipendenza dell'ordine, se possibile.

Concorrenza più facile

Detto questo, ci sono molti casi in cui non riusciamo a parallelizzare i programmi che comportano effetti collaterali in luoghi che potrebbero potenzialmente migliorare significativamente le prestazioni semplicemente a causa della possibilità che potrebbe non essere thread-safe. Uno dei casi in cui l'eliminazione dello stato mutabile (o più specificamente, gli effetti collaterali esterni) aiuta molto come vedo è che trasforma "può essere o meno sicuro per i thread" in "sicuramente sicuro per i thread" .

Per rendere tale affermazione un po 'più concreta, considera che ti do il compito di implementare una funzione di ordinamento in C che accetta un comparatore e lo usa per ordinare una matrice di elementi. È pensato per essere abbastanza generalizzato, ma ti darò una semplice ipotesi che verrà utilizzato contro input di tale scala (milioni di elementi o più) che senza dubbio sarà utile utilizzare sempre un'implementazione multithread. Puoi eseguire il multithreading della tua funzione di ordinamento?

Il problema è che non puoi perché i comparatori possono chiamare la tua funzione di ordinamentocausare effetti collaterali a meno che non si sappia come sono implementati (o quantomeno documentati) per tutti i possibili casi che è un po 'impossibile senza degeneralizzare la funzione. Un comparatore potrebbe fare qualcosa di disgustoso come modificare una variabile globale all'interno in modo non atomico. Il 99,9999% dei comparatori potrebbe non farlo, ma non possiamo ancora eseguire il multithreading di questa funzione generalizzata semplicemente a causa dello 0,00001% dei casi che potrebbero causare effetti collaterali. Di conseguenza, potrebbe essere necessario offrire una funzione di ordinamento a thread singolo e multithread e passare la responsabilità ai programmatori che la utilizzano per decidere quale utilizzare in base alla sicurezza del thread. E le persone potrebbero ancora utilizzare la versione a thread singolo e perdere opportunità di multithread perché potrebbero anche non essere sicuri che il comparatore sia thread-safe,

C'è un sacco di potere cerebrale che può essere coinvolto solo nella razionalizzazione della sicurezza del filo delle cose senza lanciare blocchi ovunque che può andare via se avessimo solo garanzie concrete che le funzioni non causeranno effetti collaterali per ora e per il futuro. E c'è la paura: la paura pratica, perché chiunque abbia dovuto eseguire il debug di una condizione di gara un numero eccessivo di volte probabilmente sarebbe titubante nel multithreading di qualcosa che non può essere sicuro al 110% di essere thread-safe e rimarrà tale. Anche per i più paranoici (di cui probabilmente sono al limite), la pura funzione fornisce quel senso di sollievo e fiducia che possiamo tranquillamente chiamare in parallelo.

E questo è uno dei casi principali in cui lo vedo così vantaggioso se puoi ottenere una dura garanzia che tali funzioni sono thread-safe che ottieni con linguaggi funzionali puri. L'altro è che i linguaggi funzionali spesso promuovono la creazione di funzioni prive di effetti collaterali in primo luogo. Ad esempio, potrebbero fornire strutture di dati persistenti in cui è abbastanza abbastanza efficiente immettere una struttura di dati di grandi dimensioni e quindi produrne una nuova con solo una piccola parte di essa modificata dall'originale senza toccare l'originale. Coloro che lavorano senza tali strutture dati potrebbero volerli modificare direttamente e perdere un po 'di sicurezza dei thread lungo il percorso.

Effetti collaterali

Detto questo, non sono d'accordo con una parte con tutto il rispetto per i miei amici funzionali (che penso siano super fighi):

[...] perché il loro paradigma evita lo stato mutabile.

Non è necessariamente l'immutabilità che rende la concorrenza così pratica come la vedo io. Sono funzioni che evitano di causare effetti collaterali. Se una funzione immette un array per ordinare, lo copia e quindi muta la copia per ordinare il suo contenuto e produce la copia, è ancora sicura come un thread che lavora con un tipo di array immutabile anche se si passa lo stesso input array ad esso da più thread. Quindi penso che ci sia ancora posto per i tipi mutabili nella creazione di codice molto favorevole alla concorrenza, per così dire, anche se ci sono molti vantaggi aggiuntivi per i tipi immutabili, tra cui strutture di dati persistenti che non uso tanto per le loro proprietà immutabili ma per eliminare il costo di dover copiare in profondità tutto per creare funzioni prive di effetti collaterali.

E c'è spesso il sovraccarico di rendere le funzioni prive di effetti collaterali sotto forma di shuffle e copia di alcuni dati aggiuntivi, forse un livello extra di indiretta, e forse alcuni GC su parti di una struttura di dati persistente, ma guardo uno dei miei amici che ha una macchina a 32 core e sto pensando che lo scambio valga probabilmente la pena se possiamo fare più cose in modo più sicuro in parallelo.


1
"Stato mutabile" significa sempre stato a livello di applicazione, non a livello di procedura. L'eliminazione di puntatori e il passaggio di parametri come valori di copia è una delle tecniche integrate in FP. Ma qualsiasi funzione utile deve mutare lo stato ad un certo livello - il punto della programmazione funzionale è garantire che lo stato mutabile che appartiene al chiamante non entri nella procedura e le mutazioni non escano dalla procedura se non dai valori di ritorno! Ma ci sono pochi programmi che possono fare molto lavoro senza mutare lo stato, e gli errori si insinuano sempre di nuovo nell'interfaccia.
Steve

1
Ad essere onesti, la maggior parte dei linguaggi moderni consente di utilizzare uno stile di programmazione funzionale (con un po 'di disciplina) e, naturalmente, ci sono linguaggi dedicati ai modelli funzionali. Ma è un modello meno efficiente dal punto di vista computazionale, ed è popolarmente esagerato come soluzione a tutti i mali tanto quanto l'orientamento agli oggetti era negli anni '90. La maggior parte dei programmi non è vincolata da calcoli ad alta intensità di CPU che trarrebbero beneficio dalla parallelizzazione, e di quelli che lo sono, è spesso a causa della difficoltà di ragionare, progettare e implementare il programma in un modo suscettibile di esecuzione parallela.
Steve

1
La maggior parte dei programmi che si occupano di stato mutevole lo fanno perché devono farlo per un motivo o per l'altro. E la maggior parte dei programmi non sono errati perché usano lo stato condiviso o lo aggiornano in modo anomalo - di solito è perché ricevono immondizia imprevista come input (che determina un output di immondizia) o perché funzionano erroneamente sui loro input (erroneamente nel senso dello scopo essere raggiunto). I modelli funzionali fanno ben poco per risolvere questo problema.
Steve

1
@Steve Potrei essere almeno a metà d'accordo con te dato che sono dalla parte di esplorare solo modi per fare le cose in modo più sicuro da linguaggi come C o C ++, e non penso davvero che dobbiamo andare al completo -blown puro funzionale per farlo. Ma trovo almeno utili alcuni concetti in FP. Ho appena finito di scrivere una risposta su come trovo utile PDS qui, e il più grande vantaggio che trovo su PDS non è in realtà la sicurezza dei thread, ma cose come instancing, editing non distruttivo, sicurezza delle eccezioni, annullamenti semplici, ecc:

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.