Cette méthode est-elle pure?

9

J'ai la méthode d'extension suivante:

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

Il applique simplement une action à chaque élément de la séquence avant de la renvoyer.

Je me demandais si je devais appliquer l' Pureattribut (des annotations Resharper) à cette méthode, et je peux voir des arguments pour et contre.

Avantages:

  • à proprement parler, il est pur; simplement l'appeler sur une séquence ne modifie pas la séquence (elle renvoie une nouvelle séquence) ou ne fait aucun changement d'état observable
  • l'appeler sans utiliser le résultat est clairement une erreur, car il n'a aucun effet à moins que la séquence ne soit énumérée, alors j'aimerais que Resharper m'avertisse si je fais cela.

Les inconvénients:

  • même si la Applyméthode elle-même est pure, l'énumération de la séquence résultante fera des changements d'état observables (ce qui est le point de la méthode). Par exemple, items.Apply(i => i.Count++)changera les valeurs des éléments à chaque fois qu'il est énuméré. Donc, appliquer l'attribut Pure est probablement trompeur ...

Qu'est-ce que tu penses? Dois-je appliquer l'attribut ou non?

Thomas Levesque
la source

Réponses:

15

Non, ce n'est pas pur, car cela a un effet secondaire. Concrètement, il fait appel actionà chaque élément. De plus, ce n'est pas threadsafe.

La propriété principale des fonctions pures est qu'elles peuvent être appelées un certain nombre de fois et ne font jamais autre chose que retourner la même valeur. Ce n'est pas votre cas. De plus, être pur signifie que vous n'utilisez rien d'autre que les paramètres d'entrée. Cela signifie qu'il peut être appelé à tout moment à partir de n'importe quel thread et ne provoquer aucun comportement inattendu. Encore une fois, ce n'est pas le cas de votre fonction.

En outre, vous pourriez vous tromper sur une chose: la pureté des fonctions n'est pas une question de pour ou de contre. Un seul doute, qu'il puisse avoir un effet secondaire, suffit à le rendre non pur.

Eric Lippert soulève un bon point. Je vais utiliser http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx dans le cadre de mon contre-argument. Surtout la ligne

Une méthode pure est autorisée à modifier les objets qui ont été créés après l'entrée dans la méthode pure.

Disons que nous créons une méthode comme celle-ci:

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

Tout d'abord, cela suppose que GetEnumeratorc'est pur aussi (je ne peux pas vraiment trouver de source à ce sujet). Si c'est le cas, alors selon la règle ci-dessus, nous pouvons annoter cette méthode avec [Pure], car elle ne modifie que l'instance qui a été créée dans le corps lui-même. Après cela, nous pouvons composer ceci et le ApplyIterator, ce qui devrait se traduire par une fonction pure, non?

Count(ApplyIterator(source, action));

Non. Cette composition n'est pas pure, même quand les deux Countet ApplyIteratorsont pures. Mais je construis peut-être cet argument sur une mauvaise prémisse. Je pense que l'idée que les instances créées dans la méthode sont exemptées de la règle de pureté est soit erronée, soit du moins pas suffisamment spécifique.

Euphorique
la source
1
La pureté de la fonction +1 n'est pas une question de pour ou de contre. La pureté fonctionnelle est un indice d'utilisation et de sécurité. Curieusement, l'OP mis en place where T : class, mais si l'OP le mettait simplement, where T : strutce serait VRAI pur.
ArT
4
Je ne suis pas d'accord avec cette réponse. L'appel sequence.Apply(action)n'a aucun effet secondaire; si c'est le cas, indiquez l'effet secondaire qu'il a. Maintenant, appeler sequence.Apply(action).GetEnumerator().MoveNext()a un effet secondaire, mais nous le savions déjà; ça mute l'énumérateur! Pourquoi devrait- sequence.Apply(action)on considérer impur, car appeler MoveNextest impur, mais sequence.Where(predicate)être considéré comme pur? sequence.Where(predicate).GetEnumerator().MoveNext()est tout aussi impur.
Eric Lippert
@EricLippert Vous soulevez un bon point. Mais ne serait-il pas suffisant d'appeler simplement GetEnumerator? Pouvons-nous considérer cela comme pur?
Euphoric
@ Euphoric: Quel effet secondaire observable produit l'appel GetEnumerator, à part l'allocation d'un énumérateur dans son état initial?
Eric Lippert
1
@EricLippert Alors pourquoi est-ce qu'Enumerable.Count est considéré comme pur par les contrats de code de .NET? Je n'ai pas de lien, mais lorsque je joue avec lui dans Visual Studio, je reçois un avertissement lorsque j'utilise un compte non pur personnalisé, mais le contrat fonctionne très bien avec Enumerable.Count.
Euphoric
18

Je ne suis pas d'accord avec les réponses d' Euphoric et de Robert Harvey . C'est absolument une fonction pure; le problème est que

Il applique simplement une action à chaque élément de la séquence avant de la renvoyer.

est très peu clair ce que signifie le premier "ça". Si "cela" signifie une de ces fonctions, alors ce n'est pas vrai; aucune de ces fonctions ne fait cela; le MoveNextde l'énumérateur de la séquence fait cela, et il "renvoie" l'élément via la Currentpropriété, pas en le renvoyant.

Ces séquences sont énumérées paresseusement , pas avec empressement , il n'est donc certainement pas le cas que l'action soit appliquée avant que la séquence ne soit renvoyée par Apply. L'action est appliquée après le retour de la séquence, si elle MoveNextest appelée sur un énumérateur.

Comme vous le constatez, ces fonctions prennent une action et une séquence et renvoient une séquence; la sortie dépend de l'entrée et aucun effet secondaire n'est produit, ce sont donc des fonctions pures.

Maintenant, si vous créez un énumérateur de la séquence résultante, puis appelez MoveNext sur cet itérateur, la méthode MoveNext n'est pas pure, car elle appelle l'action et produit un effet secondaire. Mais nous savions déjà que MoveNext n'était pas pur car il mute l'énumérateur!

Maintenant, comme pour votre question, devriez-vous appliquer l'attribut: je n'appliquerais pas l'attribut parce que je n'écrirais pas cette méthode en premier lieu . Si je veux appliquer une action à une séquence, j'écris

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

ce qui est bien clair.

Eric Lippert
la source
2
Je suppose que cette méthode tombe dans le même sac que la ForEachméthode d'extension, qui ne fait intentionnellement pas partie de Linq car son objectif est de produire des effets secondaires ...
Thomas Levesque
1
@ThomasLevesque: Mon conseil est de ne jamais faire ça . Une requête doit répondre à une question , pas muter une séquence ; c'est pourquoi on les appelle des requêtes . Muter la séquence telle qu'elle est interrogée est extrêmement dangereux . Considérez par exemple ce qui se passe si une telle requête est ensuite soumise à plusieurs appels à au Any()fil du temps; l'action sera exécutée encore et encore, mais uniquement sur le premier élément! Une séquence doit être une séquence de valeurs ; si vous voulez une séquence d' actions, faites un IEnumerable<Action>.
Eric Lippert
2
Cette réponse brouille les eaux plus qu'elle n'éclaire. Alors que tout ce que vous dites est incontestablement vrai, les principes d'immuabilité et de pureté sont des principes de langage de programmation de haut niveau, et non des détails d'implémentation de bas niveau. Les programmeurs travaillant au niveau fonctionnel sont intéressés par la façon dont leur code se comporte au niveau fonctionnel, et non par le fait que son fonctionnement interne soit pur ou non . Ils ne sont certainement pas purs sous le capot si vous descendez assez bas. Nous exécutons généralement ces choses sur l'architecture Von Neumann, qui n'est certainement pas pure.
Robert Harvey
2
@ThomasEding: La méthode n'appelle pas action, donc la pureté de actionn'est pas pertinente. Je sais qu'il ressemble à ce qu'il appelle action, mais cette méthode est un sucre syntaxique pour deux méthodes, une qui renvoie un énumérateur, et une qui est celle MoveNextde l'énumérateur. Le premier est clairement pur, et le second ne l'est clairement pas. Regardez-le de cette façon: diriez-vous que IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }c'est pur? Parce que c'est vraiment la fonction.
Eric Lippert
1
@ThomasEding: Vous manquez quelque chose; ce n'est pas ainsi que les itérateurs fonctionnent. La ApplyIteratorméthode revient immédiatement . Aucun code dans le corps de ApplyIteratorn'est exécuté jusqu'au premier appel à MoveNextl'énumérateur de l'objet renvoyé. Maintenant que vous le savez, vous pouvez déduire la réponse à ce puzzle: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… La réponse est ici: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Eric Lippert
3

Ce n'est pas une fonction pure, donc l'application de l'attribut Pure est trompeuse.

Les fonctions pures ne modifient pas la collection d'origine, et peu importe que vous passiez une action sans effet ou non; c'est toujours une fonction impure car son intention est de provoquer des effets secondaires.

Si vous souhaitez rendre la fonction pure, copiez la collection dans une nouvelle collection, appliquez les modifications apportées par l'action à la nouvelle collection et renvoyez la nouvelle collection, en laissant la collection d'origine inchangée.

Robert Harvey
la source
Eh bien, il ne modifie pas la collection d'origine, car il renvoie simplement une nouvelle séquence avec les mêmes éléments; c'est pourquoi j'envisageais de le rendre pur. Mais cela peut changer l'état des éléments lorsque vous énumérez le résultat.
Thomas Levesque
Si itemest un type de référence, il modifie la collection d'origine, même si vous retournez itemdans un itérateur. Voir stackoverflow.com/questions/1538301
Robert Harvey
1
Même s'il copiait en profondeur la collection, elle ne serait toujours pas pure, car cela actionpourrait avoir des effets secondaires autres que la modification de l'élément qui lui est transmis.
Idan Arye
@IdanArye: Certes, l'action devrait également être pure.
Robert Harvey
1
@IdanArye: ()=>{}est convertible en Action, et c'est une fonction pure. Ses sorties dépendent uniquement de ses entrées et il n'a pas d'effets secondaires observables.
Eric Lippert
0

À mon avis, le fait qu'il reçoive une action (et non quelque chose comme PureAction) ne le rend pas pur.

Et je suis même en désaccord avec Eric Lippert. Il a écrit ceci "() => {} est convertible en Action, et c'est une fonction pure. Ses sorties dépendent uniquement de ses entrées et il n'a pas d'effets secondaires observables".

Eh bien, imaginez qu'au lieu d'utiliser un délégué, ApplyIterator invoquait une méthode nommée Action.

Si l'action est pure, alors le ApplyIterator est pur aussi. Si l'action n'est pas pure, alors le ApplyIterator ne peut pas être pur.

Compte tenu du type de délégué (et non de la valeur donnée réelle), nous n'avons pas la garantie qu'il sera pur, donc la méthode se comportera comme une méthode pure uniquement lorsque le délégué est pur. Donc, pour le rendre vraiment pur, il devrait recevoir un délégué pur (et cela existe, nous pouvons déclarer un délégué comme [Pure], donc nous pouvons avoir une PureAction).

En l'expliquant différemment, une méthode Pure devrait toujours donner le même résultat avec les mêmes entrées et ne devrait pas générer de changements observables. ApplyIterator peut recevoir la même source et déléguer deux fois mais, si le délégué modifie un type de référence, la prochaine exécution donnera des résultats différents. Exemple: le délégué fait quelque chose comme item.Content + = "Changed";

Ainsi, en utilisant le ApplyIterator sur une liste de "conteneurs de chaînes" (un objet avec une propriété Content de type chaîne), nous pouvons avoir ces valeurs d'origine:

Test

Test2

Après la première exécution, la liste aura ceci:

Test Changed

Test2 Changed

Et cette 3ème fois:

Test Changed Changed

Test2 Changed Changed

Ainsi, nous modifions le contenu de la liste car le délégué n'est pas pur et aucune optimisation ne peut être effectuée pour éviter d'exécuter l'appel 3 fois s'il est appelé 3 fois, car chaque exécution générera un résultat différent.

Paulo Zemek
la source