Domanda

Uno degli argomenti che ho ascoltato contro i linguaggi funzionali è che la codifica a singolo compito è troppo difficile, o almeno significativamente più dura di "normale". programmazione.

Ma guardando attraverso il mio codice, mi sono reso conto che in realtà non ho molti (nessuno?) schemi di utilizzo che non possono essere scritti altrettanto bene usando un singolo modulo di assegnazione se stai scrivendo in un linguaggio ragionevolmente moderno.

Quindi quali sono i casi d'uso per le variabili che variano all'interno di una singola invocazione del loro ambito? Tenendo presente che gli indici di loop, i parametri e altri valori associati all'ambito che variano tra le invocazioni non sono assegnazioni multiple in questo caso (a meno che non sia necessario modificarle nel corpo per qualche motivo), e supponendo che tu stia scrivendo qualcosa di molto al di sopra del livello del linguaggio assembly, in cui puoi scrivere cose come

values.sum

o (nel caso in cui la somma non sia fornita)

function collection.sum --> inject(zero, function (v,t) --> t+v )

e

x = if a > b then a else b

o

n = case s 
  /^\d*$/ : s.to_int
  ''      : 0
  '*'     : a.length
  '?'     : a.length.random
  else    fail "I don't know how many you want"

quando è necessario e sono disponibili le informazioni sulla lista, la mappa / raccolta e così via.

Ritieni di volere / aver bisogno di variabili mutabili in tale ambiente e, in tal caso, a cosa servono?

Per chiarire, non sto chiedendo una recitazione delle obiezioni al modulo SSA, ma piuttosto esempi concreti in cui tali obiezioni sarebbero applicabili. Sto cercando frammenti di codice che siano chiari e concisi con variabili mutabili e che non possano essere scritti così senza di essi.

I miei esempi preferiti finora (e la migliore obiezione che mi aspetto):

  1. Paul Johnson's Algoritmo Fisher-Yates , che è piuttosto forte quando includi i vincoli big-O. Ma poi, come sottolinea catulahoops, il problema del big-O non è legato alla domanda SSA, ma piuttosto alla presenza di tipi di dati mutabili, e con quello messo da parte l'algoritmo può essere scritto piuttosto chiaramente in SSA:

     shuffle(Lst) ->
         array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).
     shuffle(Array, 0) -> Array;
     shuffle(Array, N) ->
         K = random:uniform(N) - 1,
         Ek = array:get(K, Array),
         En = array:get(N, Array),
         shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).
    
  2. jpalecek's area di un poligono esempio:

    def area(figure : List[Point]) : Float = {
      if(figure.empty) return 0
      val last = figure(0)
      var first= figure(0)
      val ret = 0
      for (pt <- figure) {
        ret+=crossprod(last - first, pt - first)
        last = pt
      }
      ret
    }
    

    che potrebbe ancora essere scritto qualcosa del tipo:

    def area(figure : List[Point]) : Float = {
        if figure.length < 3
            0
          else
            var a = figure(0)
            var b = figure(1)
            var c = figure(2)
            if figure.length == 3
                magnitude(crossproduct(b-a,c-a))
              else 
                foldLeft((0,a,b))(figure.rest)) { 
                   ((t,a,b),c) => (t+area([a,b,c]),a,c)
                   }
    

    Oppure, poiché alcune persone si oppongono alla densità di questa formulazione, potrebbe essere rifusa:

    def area([])    = 0.0   # An empty figure has no area
    def area([_])   = 0.0   # ...nor does a point
    def area([_,_]) = 0.0   # ...or a line segment
    def area([a,b,c]) =     # The area of a triangle can be found directly
        magnitude(crossproduct(b-a,c-a))
    def area(figure) =      # For larger figures, reduce to triangles and sum
        as_triangles(figure).collect(area).sum
    
    def as_triangles([])      = []  # No triangles without at least three points
    def as_triangles([_])     = []
    def as_triangles([_,_])   = []
    def as_triangles([a,b,c | rest) = [[a,b,c] | as_triangles([a,c | rest])]
    
  3. Il punto di Princess sulla difficoltà di implementare le code O (1) con strutture immutabili è interessante (e potrebbe fornire la base per un esempio convincente) ma, come affermato, si tratta fondamentalmente della mutabilità della struttura dei dati, e non direttamente sulla questione delle assegnazioni multiple.

  4. Sono incuriosito dalla risposta del setaccio di Eratostene, ma non convinto. Il big-O corretto, tira tutti i numeri primi che vorresti che il generatore indicato nel documento che citava non sembra facile da implementare correttamente con o senza SSA.


Bene, grazie a tutti per averci provato. Dato che la maggior parte delle risposte si è rivelata essere 1) basata su strutture di dati mutabili, non su assegnazione singola e 2) nella misura in cui riguardavano un singolo modulo di assegnazione facilmente contrastato dai professionisti esperti nella tecnica, ho intenzione di staccare la linea dai miei discorsi e / o ristrutturare (forse averlo nel backup come argomento di discussione nell'improbabile evento in cui rimango senza parole prima che finisca il tempo).

Grazie ancora.

È stato utile?

Soluzione

Non ho mai identificato un caso del genere. E mentre puoi sempre semplicemente inventare nuovi nomi, come nella conversione al modulo SSA, in realtà trovo che sia facile e naturale che ogni valore abbia il suo nome. Una lingua come Haskell mi offre molte scelte su quali valori nominare e due diversi luoghi in cui inserire i collegamenti ( let e where ). Trovo che il modulo per un singolo incarico sia del tutto naturale e per nulla difficile.

Occasionalmente mi manca la possibilità di avere puntatori su oggetti mutabili nell'heap. Ma queste cose non hanno nomi, quindi non è la stessa obiezione. (E trovo anche che quando uso oggetti mutabili sull'heap, tendo a scrivere più bug!)

Altri suggerimenti

Il problema più difficile che ho riscontrato è di mescolare un elenco. L'algoritmo Fisher-Yates (a volte noto anche come l'algoritmo Knuth) prevede di scorrere l'elenco scambiando ogni oggetto con un altro oggetto casuale. L'algoritmo è O (n), ben noto e provato da tempo (una proprietà importante in alcune applicazioni). Ma richiede array mutabili.

Questo non vuol dire che non puoi fare shuffle in un programma funzionale. Oleg Kiselyov ha scritto su questo . Ma se lo capisco correttamente, il mescolamento funzionale è O (n. Log n) perché funziona costruendo un albero binario.

Naturalmente, se avessi bisogno di scrivere l'algoritmo Fisher-Yates in Haskell, lo metterei semplicemente in ST monad , che ti permette di concludere un algoritmo che coinvolge matrici mutabili all'interno di una bella funzione pura, come questa:

-- | Implementation of the random swap algorithm for shuffling.  Reads a list
-- into a mutable ST array, shuffles it in place, and reads out the result
-- as a list.

module Data.Shuffle (shuffle) where


import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.STRef
import System.Random

-- | Shuffle a value based on a random seed.
shuffle :: (RandomGen g) => g -> [a] -> [a]
shuffle _ [] = []
shuffle g xs = 
    runST $ do
      sg <- newSTRef g
      let n = length xs
      v <- newListArray (1, n) xs
      mapM_ (shuffle1 sg v) [1..n]
      getElems v

-- Internal function to swap element i with a random element at or above it.
shuffle1 :: (RandomGen g) => STRef s g -> STArray s Int a -> Int -> ST s ()
shuffle1 sg v i = do
  (_, n) <- getBounds v
  r <- getRnd sg $ randomR (i, n)
  when (r /= i) $ do
    vi <- readArray v i
    vr <- readArray v r
    writeArray v i vr
    writeArray v r vi


-- Internal function for using random numbers
getRnd :: (RandomGen g) => STRef s g -> (g -> (a, g)) -> ST s a
getRnd sg f = do
  g1 <- readSTRef sg
  let (v, g2) = f g1
  writeSTRef sg g2
  return 

v

Se vuoi fare l'argomento accademico, ovviamente non è tecnicamente necessario assegnare una variabile più di una volta. La prova è che tutto il codice può essere rappresentato in SSA (Single Static Assignment) . In effetti, questa è la forma più utile per molti tipi di analisi statiche e dinamiche.

Allo stesso tempo, ci sono ragioni per cui non tutti scriviamo codice nel modulo SSA per cominciare:

  1. Di solito sono necessarie più istruzioni (o più righe di codice) per scrivere il codice in questo modo. La brevità ha valore.
  2. È quasi sempre meno efficiente. Sì, lo so che stai parlando di linguaggi più alti - un ambito di applicazione corretto - ma anche nel mondo di Java e C #, lontano dall'assemblaggio, la velocità è importante. Esistono poche applicazioni in cui la velocità è irrilevante.
  3. Non è così facile da capire. Sebbene SSA sia "più semplice" in senso matematico, è più astratto dal senso comune, che è ciò che conta nella programmazione del mondo reale. Se devi essere davvero intelligente per farlo, allora non ha spazio nella programmazione in generale.

Anche nei tuoi esempi sopra, è facile fare buchi. Prendi la tua dichiarazione case . Cosa succede se esiste un'opzione amministrativa che determina se '*' è consentito e uno separato se '?' è consentito? Inoltre, zero non è consentito per il caso intero, a meno che l'utente non disponga di un'autorizzazione di sistema che lo consente.

Questo è un esempio più reale con rami e condizioni. Potresti scrivere questo come una singola istruzione "quot?" In tal caso, è la tua "quotazione" " davvero diverso da molte dichiarazioni separate? In caso contrario, quante variabili di sola scrittura temporanee sono necessarie? E quella situazione è significativamente migliore che avere una sola variabile?

Penso che troverai i linguaggi più produttivi che ti permetteranno di mescolare stili funzionali e imperativi, come OCaml e F #.

Nella maggior parte dei casi, posso scrivere codice che è semplicemente una lunga riga di "mappare x a y, ridurre y a z". Nel 95% dei casi, la programmazione funzionale semplifica il mio codice, ma esiste un'area in cui l'immutabilità mostra i suoi denti:

L'ampia disparità tra facilità di implementazione e stack immutabile e una coda immutabile.

Le pile sono facili e si mescolano bene con persistenza, le code sono ridicole.

Il massimo implementazioni comuni di code immutabili utilizzano uno o più stack interni e rotazioni di stack. Il lato positivo è che queste code vengono eseguite in O (1) il più delle volte , ma alcune operazioni verranno eseguite in O (n). Se si fa affidamento sulla persistenza nell'applicazione, in linea di principio è possibile che ogni operazione venga eseguita in O (n). Queste code non vanno bene quando hai bisogno di prestazioni in tempo reale (o almeno coerenti).

Chris Okasaki fornisce un'implementazione di code immutabili in il suo libro , usano pigrizia per raggiungere O (1) per tutte le operazioni. È un'implementazione molto intelligente, ragionevolmente concisa di una coda in tempo reale, ma richiede una profonda comprensione dei dettagli di implementazione sottostanti, ed è ancora un ordine di grandezza più complesso di uno stack immutabile.

In constrast, posso scrivere uno stack e una coda utilizzando elenchi collegati mutabili che vengono eseguiti in tempo costante per tutte le operazioni e il codice risultante sarebbe molto semplice.


Per quanto riguarda l'area di un poligono, è facile convertirlo in forma funzionale. Supponiamo di avere un modulo Vector come questo:

module Vector =
    type point =
        { x : float; y : float}
        with
            static member ( + ) ((p1 : point), (p2 : point)) =
                { x = p1.x + p2.x;
                  y = p1.y + p2.y;}

            static member ( * ) ((p : point), (scalar : float)) =
                { x = p.x * scalar;
                  y = p.y * scalar;}

            static member ( - ) ((p1 : point), (p2 : point)) = 
                { x = p1.x - p2.x;
                  y = p1.y - p2.y;}

    let empty = { x = 0.; y = 0.;}
    let to_tuple2 (p : point) = (p.x, p.y)
    let from_tuple2 (x, y) = { x = x; y = y;}
    let crossproduct (p1 : point) (p2 : point) =
        { x = p1.x * p2.y; y = -p1.y * p2.x }

Possiamo definire la nostra funzione di area usando un po 'di tupla magica:

let area (figure : point list) =
    figure
    |> Seq.map to_tuple2
    |> Seq.fold
        (fun (sum, (a, b)) (c, d) -> (sum + a*d - b*c, (c, d) ) )
        (0., to_tuple2 (List.hd figure))
    |> fun (sum, _) -> abs(sum) / 2.0

Oppure possiamo usare invece il prodotto incrociato

let area2 (figure : point list) =
    figure
    |> Seq.fold
        (fun (acc, prev) cur -> (acc + (crossproduct prev cur), cur))
        (empty, List.hd figure)
    |> fun (acc, _) -> abs(acc.x + acc.y) / 2.0

Non trovo nessuna delle due funzioni illeggibili.

Quell'algoritmo shuffle è banale da implementare usando un singolo incarico, infatti è esattamente lo stesso della soluzione imperativa con l'iterazione riscritta in ricorsione della coda. (Erlang perché posso scriverlo più rapidamente di Haskell.)

 shuffle(Lst) ->
     array:to_list(shuffle(array:from_list(Lst), erlang:length(Lst) - 1)).

 shuffle(Array, 0) -> Array;
 shuffle(Array, N) ->
     K = random:uniform(N) - 1,
     Ek = array:get(K, Array),
     En = array:get(N, Array),
     shuffle(array:set(K, En, array:set(N, Ek, Array)), N-1).

Se l'efficienza di tali operazioni dell'array è un problema, allora si tratta di strutture di dati mutabili e non ha nulla a che fare con l'assegnazione singola.

Non otterrai una risposta a questa domanda perché non esistono esempi. È solo una questione di familiarità con questo stile.

In risposta a Jason -

function forbidden_input?(s)
    (s = '?' and not administration.qmark_ok) ||
    (s = '*' and not administration.stat_ok)  ||
    (s = '0' and not 'root node visible' in system.permissions_for(current_user))

n = if forbidden_input?(s)
    fail "'" + s + "' is not allowed."
  else
    case s
      /^\d*$/ : s.to_int
      ''      : 0
      '*'     : a.length
      '?'     : a.length.random
      else    fail "I don't know how many you want"

Mi mancherebbero compiti in un linguaggio non puramente funzionale. Soprattutto perché ostacolano l'utilità dei loop. Esempi (Scala):

def quant[A](x : List[A], q : A) = {
  var tmp : A=0
  for (el <- x) { tmp+= el; if(tmp > q) return el; }
  // throw exception here, there is no prefix of the list with sum > q
}

Questo dovrebbe calcolare il quantile di un elenco, notare l'accumulatore tmp che è assegnato a più volte.

Un esempio simile sarebbe:

def area(figure : List[Point]) : Float = {
  if(figure.empty) return 0
  val last = figure(0)
  var first= figure(0)
  val ret = 0
  for (pt <- figure) {
    ret+=crossprod(last - first, pt - first)
    last = pt
  }
  ret
}

Nota principalmente la variabile last .

Questi esempi potrebbero essere riscritti usando fold su una tupla per evitare compiti multipli, ma ciò non aiuterebbe davvero la leggibilità.

Le variabili

??locali (metodo) non hanno mai mai da assegnare due volte. Ma anche nella programmazione funzionale è consentita la riassegnazione di una variabile. Sta cambiando (parte di) il valore non consentito. E come già risposto dsimcha, per strutture molto grandi (forse alla radice di un'applicazione) non mi sembra possibile sostituire l'intera struttura. Pensaci. Lo stato di un'applicazione è tutto contenuto in ultima analisi dal metodo entrypoint dell'applicazione. Se assolutamente nessuno stato può cambiare senza essere sostituito, è necessario riavviare l'applicazione con ogni pressione del tasto. : (

Se si dispone di una funzione che crea un elenco / albero pigro, quindi lo riduce di nuovo, un compilatore funzionale potrebbe essere in grado di ottimizzarlo utilizzando deforestazione .

Se è complicato, potrebbe non esserlo. Quindi sei un po 'sfortunato, performance e amp; memoria saggia, a meno che tu non possa iterare e usare una variabile mutabile.

Grazie alla tesi Church-Turing, sappiamo che tutto ciò che può essere scritto in una lingua completa di Turing può essere scritto in qualsiasi lingua completa di Turing. Quindi, quando ci si arriva fino in fondo, non c'è nulla che non si possa fare in Lisp che non si possa fare in C #, se ci si è provati abbastanza, o viceversa. (Più precisamente, uno dei due verrà comunque compilato in linguaggio macchina x86 nella maggior parte dei casi.)

Quindi, la risposta alla tua domanda è: non ci sono casi del genere. Tutti ci sono casi che sono più facili da comprendere per l'uomo in un paradigma / lingua o in un altro-- e la facilità di comprensione qui è legata alla formazione e all'esperienza.

Forse il problema principale qui è lo stile del looping in una lingua. Nei linguaggi in cui viene utilizzata la ricorsione, tutti i valori che cambiano nel corso di un ciclo vengono rilegati quando viene richiamata la funzione. Le lingue che usano iteratori in blocchi (ad esempio, il metodo inject di Smalltalk e Ruby tendono ad essere simili, sebbene molte persone in Ruby userebbero comunque ciascuna e una variabile mutabile su iniettare .

Quando si eseguono cicli di codice usando mentre e per , d'altra parte, non si ha il facile riassociazione di variabili che viene automaticamente quando è possibile passare in diversi parametri per il tuo blocco di codice che esegue un'iterazione del ciclo, quindi le variabili immutabili sarebbero piuttosto scomode.

Lavorare in Haskell è un ottimo modo per indagare sulla necessità di variabili mutabili, dato che il valore predefinito è immutabile ma sono disponibili quelli mutabili (come IORefs , MVars e presto). Sono stato recentemente, ehm, "indagando". in questo modo me stesso e sono giunto alle seguenti conclusioni.

  1. Nella stragrande maggioranza dei casi, le variabili mutabili non sono necessarie e sono felice di vivere senza di esse.

  2. Per la comunicazione tra thread, le variabili mutabili sono essenziali, per ragioni abbastanza ovvie. (Questo è specifico di Haskell; i sistemi di runtime che usano il passaggio di messaggi al livello più basso non ne hanno bisogno, ovviamente.) Tuttavia, questo uso è abbastanza raro che dover usare le funzioni per leggerli e scriverli ( readIORef fooRef val ecc.) non rappresenta un grosso onere.

  3. Ho usato variabili mutabili all'interno di un singolo thread, perché sembrava rendere alcune cose più facili, ma più tardi me ne sono pentito quando mi sono reso conto che è diventato molto difficile ragionare su ciò che stava accadendo al valore memorizzato lì. (Diverse funzioni diverse stavano manipolando quel valore.) Questo è stato un po 'di apertura degli occhi; nel tipico stile rana-in-the-pot-of-warming-water, non mi ero reso conto di quanto Haskell mi avesse fatto ragionare sull'uso dei valori fino a quando non ho incontrato un esempio di come li usavo .

Quindi in questi giorni sono sceso abbastanza fermamente dalla parte delle variabili immutabili.

Poiché le precedenti risposte a questa domanda hanno confuso queste cose, mi sento in dovere di sottolineare qui con forza che questo problema è ortogonale alla purezza e alla programmazione funzionale. Sento che Ruby, ad esempio, trarrebbe beneficio dall'avere variabili locali con assegnazione singola, sebbene probabilmente alcune altre modifiche alla lingua, come l'aggiunta della ricorsione della coda, sarebbero necessarie per renderlo davvero conveniente.

Che dire di quando è necessario apportare piccole modifiche a grandi strutture di dati? Non vuoi davvero copiare un intero array o una grande classe ogni volta che modifichi alcuni elementi.

Non ci ho pensato molto, tranne ora che lo stai sottolineando.

In realtà provo a non usare più incarichi inconsciamente.

Ecco un esempio di cosa sto parlando, in python

start = self.offset%n
if start:
    start = n-start

Scritto in questo modo per evitare un Modulo o una sottrazione extra non necessari. Viene utilizzato con long ints in stile bignum, quindi è una valida ottimizzazione. La cosa a riguardo, però, è che si tratta davvero di un unico compito.

Non mi perderei affatto i compiti multipli.

So che hai richiesto un codice che mostrava i vantaggi delle variabili mutabili. E vorrei poterlo fornire. Ma come sottolineato prima, non c'è problema che non possa essere espresso in entrambe le mode. E soprattutto dal momento che hai sottolineato che l'area di un esempio di poligono di jpalecek potrebbe essere scritta con un algo pieghevole (che è un modo IMHO disordinato e porta il problema a un diverso livello di complessità) - beh, mi sono fatto meravigliare perché stai scendendo sulla mutabilità così difficile. Quindi proverò a sostenere l'argomento per un terreno comune e una coesistenza di dati immutabili e mutabili.

A mio avviso questa domanda manca un po 'il punto. So che noi programmatori siamo inclini a considerare le cose pulite e semplici, ma a volte ci manca che sia possibile anche un misto. Ed è probabilmente per questo che nella discussione sull'immutabilità raramente qualcuno prende la via di mezzo. Mi chiedo solo perché, ammettiamolo: l'immutabilità è un ottimo strumento per astrarre tutti i tipi di problemi. Ma a volte è un enorme dolore nel culo . A volte è semplicemente troppo vincolante. E questo da solo mi fa fermare e basta: vogliamo davvero perdere la mutabilità? È davvero o o? Non c'è un terreno comune a cui possiamo arrivare? Quando l'immutabilità mi aiuta a raggiungere i miei obiettivi più velocemente, quando la mutabilità? Quale soluzione è più facile da leggere e mantenere? (Quale per me è la domanda più grande)

Molte di queste domande sono influenzate dal gusto di un programmatore e da cosa sono abituate a programmare. Quindi mi concentrerò su uno degli aspetti che di solito è al centro della maggior parte degli argomenti a favore dell'immutabilità - Parallelismo:

Spesso il parallelismo viene gettato nell'argomento che circonda l'immutabilità. Ho lavorato su set di problemi che hanno richiesto la risoluzione di oltre 100 CPU in un tempo ragionevole. E mi ha insegnato una cosa molto importante: il più delle volte parallelizzare la manipolazione di grafici di dati non è in realtà il tipo di cosa che sarà il modo più efficiente di parallelizzare. Sicuramente può trarne grandi benefici, ma lo squilibrio è un vero problema in quello spazio-problema. Di solito, lavorare su più grafici mutabili in parallelo e scambiare informazioni con messaggi immutabili è molto più efficiente. Ciò significa che, quando so che il grafico è isolato, che non l'ho rivelato al mondo esterno, vorrei eseguire le mie operazioni su di esso nel modo più conciso che mi viene in mente. E questo di solito comporta la mutazione dei dati. Ma dopo queste operazioni sui dati voglio aprire i dati a tutto il mondo - e questo è il punto in cui di solito divento un po 'nervoso, se i dati sono mutabili. Poiché altre parti del programma potrebbero corrompere i dati, lo stato diventa invalido, ... perché dopo l'apertura al mondo i dati spesso entrano nel mondo del parallelismo.

Quindi i programmi paralleli del mondo reale di solito hanno aree in cui i grafici dei dati vengono utilizzati nelle operazioni definitive a thread singolo - perché semplicemente non sono noti all'esterno - e aree in cui potrebbero essere coinvolti in operazioni multi-thread (si spera solo che non forniscano dati manipolato). Durante quelle parti multi-thread non vogliamo mai che cambino - è semplicemente meglio lavorare su dati obsoleti piuttosto che su dati incoerenti. Che può essere garantito dalla nozione di immutabilità.

Questo mi ha portato a una semplice conclusione: il vero problema per me è che nessuno dei linguaggi di programmazione con cui ho familiarità mi consente di dire: " Dopo questo punto l'intera struttura dei dati sarà immutabile " e " dammi una copia mutabile di questa struttura di dati immutabile qui, verifica che solo io riesca a vedere la copia mutabile " . In questo momento devo garantirlo io stesso capovolgendo un pezzo di sola lettura o qualcosa di simile. Se potessimo avere il supporto del compilatore per esso, non solo mi garantirebbe che non ho fatto nulla di stupido dopo aver lanciato quel bit, ma potrebbe effettivamente aiutare il compilatore a fare varie ottimizzazioni che prima non poteva fare. Inoltre, il linguaggio sarebbe comunque attraente per i programmatori che hanno più familiarità con il modello di programmazione imperativo.

Quindi, per riassumere. I programmi IMHO di solito hanno una buona ragione per usare rappresentazioni sia immutabili che mutabili dei grafici dei dati . Direi che i dati dovrebbero essere immutabili per impostazione predefinita e che il compilatore dovrebbe applicarli - ma dovremmo avere la nozione di rappresentazioni mutabili private , perché ci sono naturalmente aree in cui il threading non raggiungerà mai - e la leggibilità e la manutenibilità potrebbero beneficiare di una strutturazione più imperativa.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top