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 solo un'azione a ciascun elemento della sequenza prima di restituirlo.

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

Professionisti:

  • a rigor di termini, è puro; semplicemente chiamandolo su una sequenza non altera la sequenza (restituisce una nuova sequenza) o fa qualsiasi cambiamento di stato osservabile
  • chiamarlo senza usare il risultato è chiaramente un errore, dal momento che non ha alcun effetto se non viene elencata la sequenza, quindi mi piacerebbe che Resharper mi avvertisse se lo facessi.

Contro:

  • anche se il Applymetodo stesso è puro, l'enumerazione della sequenza risultante comporterà cambiamenti di stato osservabili (che è il punto del metodo). Ad esempio, items.Apply(i => i.Count++)modificherà i valori degli elementi ogni volta che viene elencato. Quindi applicare l'attributo Pure è probabilmente fuorviante ...

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


Risposte:


15

No, non è puro, perché ha effetti collaterali. Concretamente sta chiamando actionogni oggetto. Inoltre, non è thread-safe.

La proprietà principale delle funzioni pure è che può essere chiamata un numero qualsiasi di volte e non fa altro che restituire lo stesso valore. Non è 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. Ancora una volta, questo non è il caso della tua funzione.

Inoltre, potresti sbagliarti su una cosa: la purezza delle funzioni non è una questione di pro o contro. Anche un solo dubbio, che può avere effetti collaterali, è abbastanza per renderlo non puro.

Eric Lippert solleva un buon punto. Utilizzerò http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx come parte del mio controproposto. Soprattutto linea

Un metodo puro è autorizzato a modificare oggetti che sono stati creati dopo l'entrata nel metodo puro.

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;
}

Innanzitutto, questo presuppone che GetEnumeratorsia pure (non riesco davvero a trovare alcuna fonte su questo). Se lo è, allora secondo la regola sopra, possiamo annotare questo metodo con [Pure], perché modifica solo l'istanza creata all'interno del corpo stesso. Dopodiché possiamo comporre questo e il ApplyIterator, che dovrebbe tradursi in pura funzione, giusto?

Count(ApplyIterator(source, action));

No. Questa composizione non è pura, anche se entrambe Counte ApplyIteratorsono pure. Ma potrei costruire questo argomento su premesse errate. Penso che l'idea che le istanze create all'interno del metodo siano esenti dalla regola di purezza sia sbagliata o almeno non abbastanza specifica.


1
La purezza della funzione +1 non è una questione di pro o contro. La purezza delle funzioni è un suggerimento per l'uso e la sicurezza. Stranamente l'OP ha inserito where T : class, tuttavia se l'OP lo avesse semplicemente where T : strutDOVREBBE essere puro.
Art

4
Non sono d'accordo con questa risposta. La chiamata sequence.Apply(action)non ha effetti collaterali; se lo fa, indica l'effetto collaterale che ha. Ora, chiamare sequence.Apply(action).GetEnumerator().MoveNext()ha un effetto collaterale, ma lo sapevamo già; muta l'enumeratore! Perché dovrebbe sequence.Apply(action)essere considerato impuro perché la chiamata MoveNextè impura, ma sequence.Where(predicate)essere considerata pura? sequence.Where(predicate).GetEnumerator().MoveNext()è altrettanto impuro.
Eric Lippert,

@EricLippert Hai sollevato un buon punto. Ma non basterebbe chiamare GetEnumerator? Possiamo considerarlo puro?
Euforico,

@Euforico: quale effetto collaterale osservabile produce la chiamata GetEnumerator, a parte l'allocazione di un enumeratore nel suo stato iniziale?
Eric Lippert,

1
@EricLippert Allora perché Enumerable.Count è considerato puro dai contratti di codice di .NET? Non ho link, ma quando ci gioco con Visual Studio, ricevo un avviso quando utilizzo un conteggio non puro personalizzato, ma il contratto funziona perfettamente con Enumerable.Count.
Euforico,

18

Non sono d'accordo con le risposte sia di Euforico che di Robert Harvey . Assolutamente questa è una funzione pura; il problema è che

Applica solo un'azione a ciascun elemento della sequenza prima di restituirlo.

non è molto chiaro cosa significhi il primo "esso". Se "esso" significa una di quelle funzioni, allora non è giusto; nessuna di queste funzioni lo fa; lo fa MoveNextl'enumeratore della sequenza e "restituisce" l'elemento tramite la Currentproprietà, non restituendolo.

Queste sequenze sono elencate pigramente , non avidamente, quindi non è certo il caso in cui l'azione venga applicata prima che la sequenza venga restituita Apply. L'azione viene applicata dopo la restituzione della sequenza, se MoveNextviene chiamata su un enumeratore.

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

Ora, se si crea un enumeratore della sequenza risultante e quindi si chiama MoveNext su quell'iteratore, 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, per quanto riguarda 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, allora scrivo

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

che è ben chiaro.


2
Immagino che questo metodo rientri nella stessa borsa del ForEachmetodo di estensione, che non fa intenzionalmente parte di Linq perché il suo obiettivo è produrre effetti collaterali ...
Thomas Levesque,

1
@ThomasLevesque: Il mio consiglio è di non farlo mai . Una query dovrebbe rispondere a una domanda , non mutare una sequenza ; ecco perché si chiamano query . La mutazione della sequenza quando viene interrogata è straordinariamente pericolosa . Considera ad esempio cosa succede se una query di questo tipo viene quindi sottoposta a più chiamate Any()nel tempo; l'azione verrà eseguita più volte, ma solo sul primo oggetto! Una sequenza dovrebbe essere una sequenza di valori ; se vuoi una sequenza di azioni, fai un IEnumerable<Action>.
Eric Lippert,

2
Questa risposta confonde le acque più di quanto illumina. Mentre tutto ciò che dici è indiscutibilmente vero, i principi di immutabilità e purezza sono principi del linguaggio di programmazione di alto livello, non dettagli di implementazione di basso livello. I programmatori che lavorano a livello funzionale sono interessati a come il loro codice si comporta a livello funzionale, indipendentemente dal fatto che i suoi meccanismi interni siano puri. Quasi certamente non sono puri sotto il cofano se scendi abbastanza in basso. Generalmente eseguiamo queste cose sull'architettura di Von Neumann, che certamente non è pura.
Robert Harvey,

2
@ThomasEding: il metodo non chiama action, quindi la purezza di actionè irrilevante. So che sembra che chiami action, ma questo metodo è uno zucchero sintattico per due metodi, uno che restituisce un enumeratore e uno che è MoveNextl'enumeratore. Il primo è chiaramente puro e il secondo chiaramente no. Guardalo in questo modo: diresti che IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }è puro? Perché questa è la funzione che è davvero.
Eric Lippert,

1
@ThomasEding: ti manca qualcosa; non è così che funzionano gli iteratori. Il ApplyIteratormetodo ritorna immediatamente . Nessun codice nel corpo di ApplyIteratorviene eseguito fino alla prima chiamata a MoveNextsull'enumeratore dell'oggetto restituito. Ora che lo sai, puoi dedurre la risposta a questo puzzle: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… La risposta è qui: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Eric Lippert

3

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

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

Se si desidera rendere pura la funzione, copiare la raccolta in una nuova raccolta, applicare le modifiche apportate dall'azione alla nuova raccolta e restituire la nuova raccolta, lasciando invariata la raccolta originale.


Bene, non modifica la collezione originale, poiché restituisce solo una nuova sequenza con gli stessi elementi; questo è il motivo per cui stavo considerando di renderlo puro. Ma potrebbe cambiare lo stato degli elementi quando si enumera il risultato.
Thomas Levesque,

Se itemè un tipo di riferimento, sta modificando la raccolta originale, anche se stai tornando itemin un iteratore. Vedi stackoverflow.com/questions/1538301
Robert Harvey,

1
Anche se avesse copiato in profondità la collezione, non sarebbe comunque puro, poiché actionpotrebbe avere effetti collaterali diversi dalla modifica dell'articolo che gli è stato passato.
Idan Arye,

@IdanArye: Vero, anche l'Azione dovrebbe essere pura.
Robert Harvey,

1
@IdanArye: ()=>{}è convertibile in Azione ed è una funzione pura. I suoi output dipendono esclusivamente dai suoi input e non ha effetti collaterali osservabili.
Eric Lippert,

0

Secondo me, il fatto che riceva un'azione (e non qualcosa come PureAction) non la rende pura.

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

Bene, immagina che invece di usare un delegato ApplyIterator stesse invocando 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 sarà 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 ciò esiste, possiamo dichiarare un delegato come [Puro], quindi possiamo avere una PureAction).

Spiegandolo in modo diverso, un metodo Pure dovrebbe sempre dare lo stesso risultato dati gli stessi input e non dovrebbe generare cambiamenti osservabili. A ApplyIterator può essere assegnata la stessa origine e delegato due volte ma, se il delegato sta modificando un tipo di riferimento, l'esecuzione successiva darà risultati diversi. Esempio: il delegato fa qualcosa come item.Content + = "Changed";

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

Test

Test2

Dopo la prima esecuzione, l'elenco avrà questo:

Test Changed

Test2 Changed

E questa la terza volta:

Test Changed Changed

Test2 Changed Changed

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

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.