Questo metodo è puro?

9

Ho il seguente metodo di estensione:

    public static IEnumerable<T> Apply<T>(
        [NotNull] this IEnumerable<T> source,
        [NotNull] Action<T> action)
        where T : class
    {
        source.CheckArgumentNull("source");
        action.CheckArgumentNull("action");
        return source.ApplyIterator(action);
    }

    private static IEnumerable<T> ApplyIterator<T>(this IEnumerable<T> source, Action<T> action)
        where T : class
    {
        foreach (var item in source)
        {
            action(item);
            yield return item;
        }
    }

Applica semplicemente un'azione a ciascun elemento della sequenza prima di restituirla.

Mi chiedevo se avrei dovuto applicare l'attributo Pure (dalle annotazioni di Resharper) a questo metodo, e posso vedere gli argomenti a favore e contro di esso.

Pro:

  • in senso stretto, è puro; semplicemente chiamandolo su una sequenza non altera la sequenza (restituisce una nuova sequenza) o modifica qualsiasi stato osservabile
  • chiamarlo senza usare il risultato è chiaramente un errore, dal momento che non ha alcun effetto a meno che la sequenza non sia enumerata, quindi vorrei che Resharper mi avvisi se lo faccio.

Contro:

  • anche se il metodo Apply è puro, enumerando la sequenza risultante renderà le modifiche dello stato osservabili (che è il punto del metodo). Ad esempio, items.Apply(i => i.Count++) cambierà i valori degli elementi ogni volta che viene enumerato. Quindi applicare l'attributo Pure è probabilmente fuorviante ...

Che ne pensi? Devo applicare l'attributo o no?

    
posta Thomas Levesque 26.05.2014 - 21:23
fonte

4 risposte

15

No, non è puro, perché ha un effetto collaterale. Concretamente chiama action su ogni oggetto. Inoltre, non è protetto da thread.

La principale proprietà delle funzioni pure è che può essere chiamata qualsiasi numero di volte e non fa mai altro che restituire lo stesso valore. Qual è il tuo caso. Inoltre, essere puri significa che non usi nient'altro che i parametri di input. Ciò significa che può essere chiamato da qualsiasi thread in qualsiasi momento e non causare alcun comportamento imprevisto. Di nuovo, non è il caso della tua funzione.

Inoltre, potresti essere in errore su una cosa: la purezza della funzione non è una questione di pro o contro. Anche il solo dubbio, che può avere effetti collaterali, è sufficiente a renderlo non puro.

Eric Lippert solleva un buon punto. Utilizzerò il link come parte della mia contro- discussione. Soprattutto la linea

A pure method is allowed to modify objects that have been created after entry into the pure method.

Diciamo che creiamo un metodo come questo:

int Count<T>(IEnumerable<T> e)
{
    var enumerator = e.GetEnumerator();
    int count = 0;
    while (enumerator.MoveNext()) count ++;
    return count;
}

In primo luogo, ciò presuppone che GetEnumerator sia puro (non riesco davvero a trovare alcuna fonte su questo). Se lo è, quindi in base alla regola precedente, possiamo annotare questo metodo con [Pure], perché modifica solo l'istanza creata all'interno del corpo stesso. Dopo di che possiamo comporre questo e il ApplyIterator , che dovrebbe risultare in pura funzione, giusto?

Count(ApplyIterator(source, action));

No. Questa composizione non è pura, anche se sia Count che ApplyIterator sono puri. Ma potrei costruire questo argomento su premesse sbagliate. Penso che l'idea che le istanze create all'interno del metodo siano esenti dalla regola di purezza è errata o almeno non abbastanza specifica.

    
risposta data 26.05.2014 - 21:33
fonte
18

Non sono d'accordo con entrambi Euforico e risposte di Robert Harvey . Assolutamente quella è una pura funzione; il problema è che

It just applies an action to each item of the sequence before returning it.

non è chiaro quale sia il primo "it". Se "it" significa una di quelle funzioni, allora non è giusto; nessuna di queste funzioni lo fa; il MoveNext dell'enumeratore della sequenza lo fa e "restituisce" l'oggetto tramite la proprietà Current , non restituendola.

Quelle sequenze sono enumerate lazily , non con entusiasmo quindi non è certamente il caso che l'azione sia applicata prima la sequenza sia restituita da %codice%. L'azione viene applicata dopo la sequenza viene restituita, se Apply viene richiamato su un enumeratore.

Come noti, queste funzioni eseguono un'azione e una sequenza e restituiscono una sequenza; l'output dipende dall'input e non vengono prodotti effetti collaterali, quindi queste sono pure funzioni ..

Ora, se si crea un enumeratore della sequenza risultante e si chiama MoveNext su quell'iteratore, allora il metodo MoveNext non è puro, perché chiama l'azione e produce un effetto collaterale. Ma sapevamo già che MoveNext non era puro perché muta l'enumeratore!

Ora, come per la tua domanda, dovresti applicare l'attributo: non applicherei l'attributo perché non scriverei questo metodo in primo luogo . Se voglio applicare un'azione a una sequenza, scrivo

foreach(var item in sequence) action(item);

che è ben chiaro.

    
risposta data 06.06.2014 - 18:26
fonte
3

Non è una funzione pura, quindi applicare l'attributo Pure è fuorviante.

Le funzioni pure non modificano la raccolta originale e non importa se stai passando un'azione senza effetti o no; è ancora una funzione impura perché il suo intento è causare effetti collaterali.

Se vuoi rendere pura la funzione, copia la raccolta in una nuova raccolta, applica le modifiche che l'Azione porta alla nuova raccolta e restituisci la nuova raccolta, lasciando invariata la raccolta originale.

    
risposta data 26.05.2014 - 21:35
fonte
0

A mio parere, il fatto che riceva un'azione (e non qualcosa come PureAction) non lo rende puro.

E non sono nemmeno d'accordo con Eric Lippert. Ha scritto questo "() = > {} è convertibile in Action, ed è una funzione pura: gli output dipendono esclusivamente dai suoi input e non ha effetti collaterali osservabili".

Bene, immagina che invece di usare un delegato, ApplyIterator invocasse un metodo chiamato Action.

Se Action è puro, anche ApplyIterator è puro. Se Action non è puro, allora ApplyIterator non può essere puro.

Considerando il tipo di delegato (non il valore dato effettivo), non abbiamo la garanzia che sia puro, quindi il metodo si comporterà come metodo puro solo quando il delegato è puro. Quindi, per renderlo veramente puro, dovrebbe ricevere un delegato puro (e che esiste, possiamo dichiarare un delegato come [Pure], così possiamo avere una PureAction).

Spiegandolo in modo diverso, un metodo Pure dovrebbe sempre dare lo stesso risultato dato gli stessi input e non dovrebbe generare cambiamenti osservabili. ApplyIterator può ricevere la stessa fonte e delegare due volte ma, se il delegato sta cambiando un tipo di riferimento, l'esecuzione successiva darà risultati diversi. Esempio: il delegato esegue qualcosa come item.Content +="Modificato";

Quindi, usando ApplyIterator su un elenco di "contenitori stringa" (un oggetto con una proprietà Content di tipo stringa), potremmo avere questi valori originali:

Test

Test2

Dopo la prima esecuzione, la lista avrà questo:

Test Changed

Test2 Changed

E questa è la terza volta:

Test Changed Changed

Test2 Changed Changed

Quindi, stiamo cambiando il contenuto dell'elenco perché il delegato non è puro e non è possibile eseguire alcuna ottimizzazione per evitare di eseguire la chiamata 3 volte se invocato 3 volte, poiché ogni esecuzione genererà un risultato diverso.

    
risposta data 20.08.2014 - 20:10
fonte

Leggi altre domande sui tag