Ist diese Methode rein?

9

Ich habe die folgende Erweiterungsmethode:

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

Es wird lediglich eine Aktion auf jedes Element der Sequenz angewendet, bevor es zurückgegeben wird.

Ich habe mich gefragt, ob ich das PureAttribut (aus Resharper-Annotationen) auf diese Methode anwenden soll , und ich kann Argumente dafür und dagegen sehen.

Vorteile:

  • streng genommen, es ist rein; Wenn Sie es nur für eine Sequenz aufrufen, ändert sich weder die Sequenz (es wird eine neue Sequenz zurückgegeben) noch wird eine beobachtbare Statusänderung vorgenommen
  • Das Aufrufen ohne Verwendung des Ergebnisses ist eindeutig ein Fehler, da es keine Auswirkung hat, wenn die Sequenz nicht aufgezählt wird. Daher möchte ich, dass Resharper mich warnt, wenn ich das tue.

Nachteile:

  • obwohl die ApplyMethode selbst rein ist, die resultierende Sequenz Aufzählen wird machen beobachtbaren Zustandsänderungen (das ist der Punkt des Verfahrens ist). Ändert beispielsweise items.Apply(i => i.Count++)die Werte der Elemente jedes Mal, wenn sie aufgelistet werden. Das Anwenden des Pure-Attributs ist also wahrscheinlich irreführend ...

Was denkst du? Soll ich das Attribut anwenden oder nicht?

Thomas Levesque
quelle

Antworten:

15

Nein, es ist nicht rein, weil es Nebenwirkungen hat. Konkret ruft es actionjeden Gegenstand auf. Es ist auch nicht threadsicher.

Die Haupteigenschaft reiner Funktionen ist, dass sie beliebig oft aufgerufen werden können und niemals etwas anderes tun, als denselben Wert zurückzugeben. Welches ist nicht dein Fall. Rein zu sein bedeutet auch, dass Sie nichts anderes als die Eingabeparameter verwenden. Dies bedeutet, dass es jederzeit von jedem Thread aus aufgerufen werden kann und kein unerwartetes Verhalten verursacht. Auch dies ist bei Ihrer Funktion nicht der Fall.

Sie könnten sich auch in einer Sache irren: Funktionsreinheit ist keine Frage der Vor- oder Nachteile. Selbst ein einziger Zweifel, dass es Nebenwirkungen haben kann, reicht aus, um es nicht rein zu machen.

Eric Lippert spricht einen guten Punkt an. Ich werde http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx als Teil meines Gegenarguments verwenden. Besonders Linie

Eine reine Methode darf Objekte ändern, die nach dem Eintritt in die reine Methode erstellt wurden.

Nehmen wir an, wir erstellen eine Methode wie diese:

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

Erstens setzt dies voraus, dass dies auch GetEnumeratorrein ist (ich kann keine Quelle dafür finden). Wenn dies der Fall ist, können wir diese Methode gemäß der obigen Regel mit [Pure] kommentieren, da nur die Instanz geändert wird, die im Hauptteil selbst erstellt wurde. Danach können wir dies und das komponieren ApplyIterator, was zu einer reinen Funktion führen sollte, oder?

Count(ApplyIterator(source, action));

Nein. Diese Zusammensetzung ist nicht rein, auch wenn beide Countund ApplyIteratorrein sind. Aber ich könnte dieses Argument auf einer falschen Prämisse aufbauen. Ich denke, dass die Idee, dass Instanzen, die innerhalb der Methode erstellt wurden, von der Reinheitsregel ausgenommen sind, entweder falsch oder zumindest nicht spezifisch genug ist.

Euphorisch
quelle
1
+1 Funktionsreinheit ist keine Frage der Vor- oder Nachteile. Funktionsreinheit ist ein Hinweis auf Verwendung und Sicherheit. Seltsamerweise where T : classwürde das OP where T : strutjedoch rein sein , wenn das OP es einfach formulierte .
ArTs
4
Ich bin mit dieser Antwort nicht einverstanden. Anrufen sequence.Apply(action)hat keine Nebenwirkung; Wenn dies der Fall ist, geben Sie die Nebenwirkung an, die es hat. Jetzt hat das Anrufen sequence.Apply(action).GetEnumerator().MoveNext()einen Nebeneffekt, aber das wussten wir bereits. es mutiert den Enumerator! Warum sollte sequence.Apply(action)man es als unrein betrachten, weil das Rufen MoveNextunrein ist, aber sequence.Where(predicate)als rein angesehen werden? sequence.Where(predicate).GetEnumerator().MoveNext()ist genauso unrein.
Eric Lippert
@EricLippert Sie sprechen einen guten Punkt an. Aber würde es nicht ausreichen, nur GetEnumerator aufzurufen? Können wir das als rein betrachten?
Euphoric
@Euphoric: Welche beobachtbaren Nebenwirkungen hat der Aufruf GetEnumerator, abgesehen von der Zuweisung eines Enumerators im Ausgangszustand?
Eric Lippert
1
@EricLippert Warum wird Enumerable.Count dann von .NETs Codeverträgen als rein angesehen? Ich habe keinen Link, aber wenn ich in Visual Studio damit spiele, erhalte ich eine Warnung, wenn ich eine benutzerdefinierte nicht reine Zählung verwende, aber der Vertrag funktioniert mit Enumerable.Count einwandfrei.
Euphoric
18

Ich bin mit den Antworten von Euphoric und Robert Harvey nicht einverstanden . Absolut das ist eine reine Funktion; Das Problem ist, dass

Es wird lediglich eine Aktion auf jedes Element der Sequenz angewendet, bevor es zurückgegeben wird.

ist sehr unklar, was das erste "es" bedeutet. Wenn "es" eine dieser Funktionen bedeutet, dann ist das nicht richtig; Keine dieser Funktionen macht das; Der MoveNextEnumerator der Sequenz tut dies und "gibt" das Element über die CurrentEigenschaft zurück, nicht durch Rückgabe.

Diese Sequenzen werden träge und nicht eifrig aufgezählt , so dass es sicherlich nicht der Fall ist, dass die Aktion angewendet wird, bevor die Sequenz von zurückgegeben wird Apply. Die Aktion wird angewendet, nachdem die Sequenz zurückgegeben wurde, wenn MoveNextsie auf einem Enumerator aufgerufen wird.

Wie Sie bemerken, führen diese Funktionen eine Aktion und eine Sequenz aus und geben eine Sequenz zurück. Die Ausgabe hängt von der Eingabe ab und es werden keine Nebenwirkungen erzeugt. Dies sind also reine Funktionen.

Wenn Sie nun einen Enumerator für die resultierende Sequenz erstellen und dann MoveNext für diesen Iterator aufrufen, ist die MoveNext-Methode nicht rein, da sie die Aktion aufruft und einen Nebeneffekt erzeugt. Aber wir wussten bereits, dass MoveNext nicht rein ist, weil es den Enumerator mutiert!

Was nun Ihre Frage betrifft, sollten Sie das Attribut anwenden: Ich würde das Attribut nicht anwenden, da ich diese Methode überhaupt nicht schreiben würde . Wenn ich eine Aktion auf eine Sequenz anwenden möchte, schreibe ich

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

das ist schön klar.

Eric Lippert
quelle
2
Ich denke, diese Methode fällt in die gleiche Tasche wie die ForEachVerlängerungsmethode, die absichtlich nicht Teil von Linq ist, weil ihr Ziel darin besteht, Nebenwirkungen zu erzeugen ...
Thomas Levesque
1
@ThomasLevesque: Mein Rat ist, das niemals zu tun . Eine Abfrage sollte eine Frage beantworten und keine Sequenz mutieren . Deshalb werden sie Abfragen genannt . Das Mutieren der abgefragten Sequenz ist außerordentlich gefährlich . Stellen Sie sich zum Beispiel vor, was passiert, wenn eine solche Abfrage im Any()Laufe der Zeit mehreren Aufrufen unterzogen wird. Die Aktion wird immer wieder ausgeführt, jedoch nur beim ersten Gegenstand! Eine Sequenz sollte eine Folge von Werten sein . Wenn Sie eine Abfolge von Aktionen wünschen, machen Sie eine IEnumerable<Action>.
Eric Lippert
2
Diese Antwort trübt das Wasser mehr als es beleuchtet. Während alles, was Sie sagen, zweifellos wahr ist, sind die Prinzipien der Unveränderlichkeit und Reinheit Prinzipien der Programmiersprache auf hoher Ebene, keine Implementierungsdetails auf niedriger Ebene. Programmierer, die auf funktionaler Ebene arbeiten, sind daran interessiert, wie sich ihr Code auf funktionaler Ebene verhält, und nicht daran, ob das Innenleben rein ist oder nicht . Sie sind mit ziemlicher Sicherheit nicht rein unter der Haube, wenn Sie tief genug gehen. Wir alle führen diese Dinge im Allgemeinen auf der Von Neumann-Architektur aus, die mit Sicherheit nicht rein ist.
Robert Harvey
2
@ThomasEding: Die Methode ruft nicht auf action, daher ist die Reinheit von actionirrelevant. Ich weiß, es sieht so aus, als würde es aufgerufen action, aber diese Methode ist ein syntaktischer Zucker für zwei Methoden, eine, die einen Enumerator zurückgibt, und eine, die MoveNextder Enumerator ist. Ersteres ist eindeutig rein und Letzteres eindeutig nicht. Betrachten Sie es so: Würden Sie sagen, dass IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }das rein ist? Denn das ist die Funktion, die das wirklich ist.
Eric Lippert
1
@ThomasEding: Ihnen fehlt etwas; So funktionieren Iteratoren nicht. Die ApplyIteratorMethode kehrt sofort zurück . ApplyIteratorBis zum ersten Aufruf MoveNextdes Enumerators des zurückgegebenen Objekts wird kein Code im Hauptteil von ausgeführt . Nachdem Sie das wissen, können Sie die Antwort auf dieses Rätsel ableiten: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… Die Antwort lautet hier: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Eric Lippert
3

Es ist keine reine Funktion, daher ist die Anwendung des Pure-Attributs irreführend.

Reine Funktionen ändern die ursprüngliche Sammlung nicht und es spielt keine Rolle, ob Sie eine Aktion übergeben, die keine Auswirkung hat oder nicht. Es ist immer noch eine unreine Funktion, weil es Nebenwirkungen verursachen soll.

Wenn Sie die Funktion rein machen möchten, kopieren Sie die Sammlung in eine neue Sammlung, wenden Sie die von der Aktion vorgenommenen Änderungen auf die neue Sammlung an und geben Sie die neue Sammlung zurück, wobei die ursprüngliche Sammlung unverändert bleibt.

Robert Harvey
quelle
Nun, die ursprüngliche Sammlung wird nicht geändert, da nur eine neue Sequenz mit denselben Elementen zurückgegeben wird. Deshalb habe ich darüber nachgedacht, es rein zu machen. Es kann jedoch den Status der Elemente ändern, wenn Sie das Ergebnis aufzählen.
Thomas Levesque
Wenn itemes sich um einen Referenztyp handelt, wird die ursprüngliche Sammlung geändert, obwohl Sie itemin einem Iterator zurückkehren. Siehe stackoverflow.com/questions/1538301
Robert Harvey
1
Selbst wenn er die Sammlung tief kopiert hätte, wäre sie immer noch nicht rein, da actionsie andere Nebenwirkungen haben könnte, als das an sie übergebene Objekt zu ändern.
Idan Arye
@IdanArye: Stimmt, die Aktion müsste auch rein sein.
Robert Harvey
1
@IdanArye: ()=>{}ist in Action konvertierbar und eine reine Funktion. Die Ergebnisse hängen ausschließlich von den Eingaben ab und es treten keine beobachtbaren Nebenwirkungen auf.
Eric Lippert
0

Meiner Meinung nach macht die Tatsache, dass es eine Aktion erhält (und nicht so etwas wie PureAction), es nicht rein.

Und ich bin sogar anderer Meinung als Eric Lippert. Er schrieb: "() => {} ist in Action konvertierbar und eine reine Funktion. Die Ausgaben hängen ausschließlich von den Eingaben ab und es gibt keine beobachtbaren Nebenwirkungen."

Stellen Sie sich vor, der ApplyIterator hat anstelle eines Delegaten eine Methode namens Action aufgerufen.

Wenn Action rein ist, ist auch der ApplyIterator rein. Wenn Action nicht rein ist, kann der ApplyIterator nicht rein sein.

In Anbetracht des Delegatentyps (nicht des tatsächlich angegebenen Werts) können wir nicht garantieren, dass er rein ist. Daher verhält sich die Methode nur dann als reine Methode, wenn der Delegat rein ist. Um es wirklich rein zu machen, sollte es einen reinen Delegaten erhalten (und das existiert, wir können einen Delegaten als [Pure] deklarieren, damit wir eine PureAction haben können).

Anders ausgedrückt sollte eine Pure-Methode bei gleichen Eingaben immer das gleiche Ergebnis liefern und keine beobachtbaren Änderungen erzeugen. ApplyIterator kann dieselbe Quelle und denselben Delegaten zweimal erhalten. Wenn der Delegat jedoch einen Referenztyp ändert, führt die nächste Ausführung zu unterschiedlichen Ergebnissen. Beispiel: Der Delegat macht so etwas wie item.Content + = "Changed";

Wenn Sie also den ApplyIterator über eine Liste von "Zeichenfolgencontainern" (ein Objekt mit einer Content-Eigenschaft vom Typ Zeichenfolge) verwenden, haben wir möglicherweise die folgenden ursprünglichen Werte:

Test

Test2

Nach der ersten Ausführung enthält die Liste Folgendes:

Test Changed

Test2 Changed

Und das zum 3. Mal:

Test Changed Changed

Test2 Changed Changed

Daher ändern wir den Inhalt der Liste, da der Delegat nicht rein ist und keine Optimierung durchgeführt werden kann, um zu vermeiden, dass der Aufruf dreimal ausgeführt wird, wenn er dreimal aufgerufen wird, da jede Ausführung ein anderes Ergebnis generiert.

Paulo Zemek
quelle