Czy ta metoda jest czysta?

9

Mam następującą metodę rozszerzenia:

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

Po prostu stosuje akcję do każdego elementu sekwencji przed jej zwróceniem.

Zastanawiałem się, czy powinienem zastosować Pureatrybut (z adnotacji Resharper) do tej metody i widzę argumenty za i przeciw.

Plusy:

  • ściśle mówiąc, jest czysty; samo wywołanie go w sekwencji nie zmienia sekwencji (zwraca nową sekwencję) ani nie powoduje zmiany obserwowalnego stanu
  • wywołanie go bez użycia wyniku jest oczywiście błędem, ponieważ nie ma żadnego efektu, chyba że sekwencja jest wymieniona, dlatego chciałbym, aby Resharper mnie ostrzegł, jeśli to zrobię.

Cons:

  • nawet jeśli Applysama metoda jest czysta, wyliczenie wynikowej sekwencji spowoduje obserwowalne zmiany stanu (co jest celem metody). Na przykład items.Apply(i => i.Count++)zmieni wartości elementów za każdym razem, gdy są wyliczane. Zatem zastosowanie atrybutu Pure prawdopodobnie wprowadza w błąd ...

Co myślisz? Czy powinienem zastosować ten atrybut, czy nie?

Thomas Levesque
źródło

Odpowiedzi:

15

Nie, to nie jest czyste, ponieważ ma efekt uboczny. Konkretnie, wzywa actiondo każdego przedmiotu. Ponadto nie jest wątkowo bezpieczny.

Główną właściwością funkcji czystych jest to, że można ją wywoływać dowolną liczbę razy i nigdy nie robi nic poza zwracaniem tej samej wartości. Co nie jest twoją sprawą. Ponadto bycie czystym oznacza, że ​​nie używasz niczego poza parametrami wejściowymi. Oznacza to, że można go wywołać z dowolnego wątku w dowolnym momencie i nie powodować żadnych nieoczekiwanych zachowań. Ponownie, to nie jest przypadek twojej funkcji.

Być może mylisz się co do jednej rzeczy: czystość funkcji nie jest kwestią zalet ani wad. Nawet jedna wątpliwość, że może mieć efekt uboczny, wystarczy, aby nie był czysty.

Eric Lippert podnosi dobry punkt. Będę używał http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx jako części mojego kontrargumentu. Zwłaszcza linia

Metoda czysta może modyfikować obiekty utworzone po wejściu do metody czystej.

Powiedzmy, że tworzymy taką metodę:

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

Po pierwsze, zakłada się, że GetEnumeratorto też jest czyste (tak naprawdę nie mogę znaleźć na to żadnego źródła). Jeśli tak, to zgodnie z powyższą regułą możemy opisać tę metodę za pomocą [Pure], ponieważ modyfikuje ona tylko instancję utworzoną w samym ciele. Następnie możemy skomponować to i to ApplyIterator, co powinno dać czystą funkcję, prawda?

Count(ApplyIterator(source, action));

Ilość ta kompozycja nie jest czysty, nawet gdy oba Counti ApplyIteratorsą czyste. Ale może buduję ten argument na złych przesłankach. Myślę, że idea, że ​​instancje utworzone w ramach tej metody są zwolnione z zasady czystości, jest albo błędna, albo przynajmniej nie dość szczegółowa.

Euforyk
źródło
1
Czystość funkcji +1 nie jest kwestią zalet ani wad. Czystość funkcji jest wskazówką dotyczącą użytkowania i bezpieczeństwa. Dziwne, że włożył OP where T : class, ale jeśli OP po prostu where T : strutto powiedzi , BYŁoby czyste.
ArTs
4
Nie zgadzam się z tą odpowiedzią. Dzwonienie sequence.Apply(action)nie ma skutków ubocznych; jeśli tak, podaj efekt uboczny, który ma. Teraz dzwonienie sequence.Apply(action).GetEnumerator().MoveNext()ma efekt uboczny, ale już to wiedzieliśmy; mutuje moduł wyliczający! Dlaczego należy sequence.Apply(action)uważać go za nieczystego, ponieważ dzwonienie MoveNextjest nieczyste, a sequence.Where(predicate)za czyste? sequence.Where(predicate).GetEnumerator().MoveNext()jest równie nieczyste.
Eric Lippert,
@EricLippert Podnosisz dobry punkt. Ale czy nie wystarczy po prostu wywołać GetEnumerator? Czy możemy uznać to za czyste?
Euforyczny
@Euphoric: Jakie zauważalne skutki uboczne wywołuje GetEnumeratorwywoływanie, poza przydzieleniem licznika w jego początkowym stanie?
Eric Lippert,
1
@EricLippert Dlaczego więc to, że Enumerable.Count jest uważany za czysty przez kontrakty kodowe .NET? Nie mam linku, ale kiedy gram z nim w studiu wizualnym, pojawia się ostrzeżenie, gdy używam niestandardowego nieczystego liczenia, ale umowa działa dobrze z Enumerable.Count.
Euforyczny
18

Nie zgadzam się zarówno z odpowiedziami Euforii, jak i Roberta Harveya . Oczywiście jest to czysta funkcja; problemem jest

Po prostu stosuje akcję do każdego elementu sekwencji przed jej zwróceniem.

jest bardzo niejasne, co oznacza pierwsze „to”. Jeśli „to” oznacza jedną z tych funkcji, to nie jest właściwe; żadna z tych funkcji tego nie robi; MoveNextz wyliczający sekwencji robi, i it „zwraca” pozycja za pośrednictwem Currentnieruchomości, a nie poprzez jego zwrot.

Sekwencje te są wyliczane leniwie , nie chętnie, więc z pewnością nie jest tak, że akcja jest wykonywana przed zwróceniem sekwencji Apply. Akcja jest stosowana po zwróceniu sekwencji, jeśli MoveNextzostanie wywołana w module wyliczającym.

Jak zauważasz, funkcje te podejmują akcję i sekwencję i zwracają sekwencję; wynik zależy od danych wejściowych i nie występują żadne skutki uboczne, więc są to wyłącznie funkcje.

Teraz, jeśli utworzysz moduł wyliczający wynikową sekwencję, a następnie wywołasz MoveNext na tym iteratorze, wówczas metoda MoveNext nie będzie czysta, ponieważ wywołuje akcję i wywołuje efekt uboczny. Ale już wiedzieliśmy, że MoveNext nie był czysty, ponieważ mutuje moduł wyliczający!

Teraz, jeśli chodzi o twoje pytanie, czy powinieneś zastosować atrybut: Nie zastosowałbym tego atrybutu, ponieważ nie napisałbym tej metody w pierwszej kolejności . Jeśli chcę zastosować akcję do sekwencji, piszę

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

co jest całkiem jasne.

Eric Lippert
źródło
2
Myślę, że ta metoda mieści się w tej samej torbie co ForEachmetoda przedłużania, która celowo nie jest częścią Linq, ponieważ jej celem jest wywoływanie efektów ubocznych ...
Thomas Levesque
1
@ThomasLevesque: Moja rada to nigdy tego nie robić . Zapytanie powinno odpowiedzieć na pytanie , a nie mutować sekwencji ; dlatego nazywane są zapytaniami . Mutowanie sekwencji podczas zapytania jest wyjątkowo niebezpieczne . Zastanówmy się na przykład, co się stanie, jeśli takie zapytanie zostanie Any()z czasem poddane wielokrotnym wywołaniom ; akcja będzie wykonywana raz po raz, ale tylko na pierwszym elemencie! Sekwencja powinna być sekwencją wartości ; jeśli chcesz sekwencji działań, wykonaj IEnumerable<Action>.
Eric Lippert,
2
Ta odpowiedź bardziej zamazuje wody niż oświetla. Chociaż wszystko, co mówisz, jest bezsprzecznie prawdziwe, zasady niezmienności i czystości są zasadami wysokiego poziomu języka programowania, a nie szczegółami implementacji niskiego poziomu. Programiści pracujący na poziomie funkcjonalnym są zainteresowani tym, jak ich kod zachowuje się na poziomie funkcjonalnym, a nie to, czy jego wewnętrzne działanie jest czyste. Prawie na pewno nie są czyste pod maską, jeśli zejdziesz wystarczająco nisko. Wszyscy na ogół uruchamiamy te rzeczy w architekturze von Neumanna, która z pewnością nie jest czysta.
Robert Harvey
2
@ThomasEding: Metoda nie wywołuje action, więc czystość nie actionma znaczenia. Wiem, że wygląda na to action, że wywołuje , ale ta metoda jest cukrem składniowym dla dwóch metod, jednej, która zwraca moduł wyliczający, i drugiej, która jest MoveNextmechanizmem wyliczającym. Pierwsza jest wyraźnie czysta, a druga wyraźnie nie jest. Spójrz na to w ten sposób: czy powiedziałbyś, że IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }to czyste? Ponieważ tak naprawdę jest to funkcja.
Eric Lippert,
1
@ThomasEding: Coś brakuje; nie tak działają iteratory. ApplyIteratorMetoda zwraca natychmiast . Żaden kod w treści nie ApplyIteratorjest uruchamiany do czasu pierwszego wywołania MoveNextw module wyliczającym zwróconego obiektu. Teraz, gdy już o tym wiesz, możesz wydedukować odpowiedź na tę zagadkę: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… Odpowiedź znajduje się tutaj: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Eric Lippert
3

To nie jest czysta funkcja, więc stosowanie atrybutu Pure wprowadza w błąd.

Czyste funkcje nie modyfikują oryginalnej kolekcji i nie ma znaczenia, czy przekazujesz akcję, która nie ma efektu, czy nie; wciąż jest nieczystą funkcją, ponieważ jej celem jest wywoływanie skutków ubocznych.

Jeśli chcesz, aby funkcja była czysta, skopiuj kolekcję do nowej kolekcji, zastosuj zmiany wprowadzone w Akcji do nowej kolekcji i zwróć nową kolekcję, pozostawiając oryginalną kolekcję bez zmian.

Robert Harvey
źródło
Cóż, nie modyfikuje oryginalnej kolekcji, ponieważ po prostu zwraca nową sekwencję z tymi samymi elementami; właśnie dlatego rozważałem uczynienie go czystym. Ale może to zmienić stan elementów podczas wyliczania wyniku.
Thomas Levesque
Jeśli itemjest typem odniesienia, modyfikuje oryginalną kolekcję, nawet jeśli powracasz itemw iteratorze. Zobacz stackoverflow.com/questions/1538301
Robert Harvey
1
Nawet jeśli głęboko skopiował kolekcję, nadal nie byłaby czysta, ponieważ actionmoże mieć skutki uboczne inne niż modyfikacja przekazywanego do niej przedmiotu.
Idan Arye
@IdanArye: To prawda, że ​​akcja również musiałaby być czysta.
Robert Harvey
1
@IdanArye: ()=>{}jest konwertowany na Action i jest czystą funkcją. Jego wyniki zależą wyłącznie od jego nakładów i nie ma zauważalnych efektów ubocznych.
Eric Lippert,
0

Moim zdaniem fakt otrzymania akcji (a nie czegoś takiego jak PureAction) sprawia, że ​​nie jest ona czysta.

I nawet nie zgadzam się z Erikiem Lippertem. Napisał on: „() => {} można przekształcić w Action i jest to czysta funkcja. Jego wyniki zależą wyłącznie od danych wejściowych i nie ma żadnych zauważalnych efektów ubocznych”.

Wyobraź sobie, że zamiast użyć delegata, ApplyIterator wywoływał metodę o nazwie Action.

Jeśli akcja jest czysta, wówczas ApplyIterator jest również czysty. Jeśli Action nie jest czyste, wówczas ApplyIterator nie może być czysty.

Biorąc pod uwagę typ delegowanego (a nie faktyczną podaną wartość), nie mamy gwarancji, że będzie czysty, więc metoda będzie zachowywać się jak czysta metoda tylko wtedy, gdy delegowany jest czysty. Tak więc, aby było naprawdę czyste, powinien otrzymać czystego delegata (i że istnieje, możemy zadeklarować delegata jako [Pure], abyśmy mogli mieć PureAction).

Wyjaśniając to inaczej, metoda Pure powinna zawsze dawać ten sam wynik przy tych samych danych wejściowych i nie powinna generować obserwowalnych zmian. ApplyIterator może otrzymać dwa razy to samo źródło i delegować, ale jeśli delegat zmienia typ odwołania, następne wykonanie da inne wyniki. Przykład: Delegat robi coś takiego jak item.Content + = "Changed";

Tak więc, stosując ApplyIterator na liście „kontenerów ciągów” (obiekt z właściwością Content typu ciąg znaków), możemy mieć te oryginalne wartości:

Test

Test2

Po pierwszym wykonaniu lista będzie miała:

Test Changed

Test2 Changed

A to trzeci raz:

Test Changed Changed

Test2 Changed Changed

Tak więc zmieniamy zawartość listy, ponieważ delegat nie jest czysty i nie można przeprowadzić optymalizacji, aby uniknąć wykonania połączenia 3 razy, jeśli zostanie wywołany 3 razy, ponieważ każde wykonanie wygeneruje inny wynik.

Paulo Zemek
źródło