Чи чистий цей метод?


9

У мене є такий метод розширення:

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

Він просто застосовує дію до кожного елемента послідовності перед поверненням.

Мені було цікаво, чи слід застосувати Pureатрибут (від анотацій Resharper) до цього методу, і я можу побачити аргументи «за» і «проти».

Плюси:

  • строго кажучи, є чистим; просто виклик його в послідовності не змінює послідовність (вона повертає нову послідовність) або не вносить будь-яких змін, що спостерігаються
  • називати його без використання результату, очевидно, є помилкою, оскільки це не має ефекту, якщо не буде перерахована послідовність, тому я хотів би, щоб Решарпер попередив мене, якщо це зробити.

Мінуси:

  • навіть якщо Applyсам метод чистий, перерахування отриманої послідовності буде зробити спостерігаються зміни стану (яка є точкою методи). Наприклад, items.Apply(i => i.Count++)буде змінювати значення елементів кожного разу, коли він перераховується. Тож застосування атрибута Pure, ймовірно, вводить в оману ...

Як ти гадаєш? Потрібно застосувати атрибут чи ні?


Відповіді:


15

Ні, він не чистий, тому що має побічну дію. Конкретно це заклик actionдо кожного товару. Крім того, це не є безпечним для ниток.

Основна властивість чистих функцій полягає в тому, що її можна викликати будь-яку кількість разів, і вона ніколи не робить нічого іншого, ніж повертати те саме значення. Що не у вашому випадку. Крім того, будучи чистою, ви не використовуєте нічого, крім вхідних параметрів. Це означає, що його можна викликати з будь-якої потоку в будь-який час і не викликати несподіваної поведінки. Знову ж таки, це не стосується вашої функції.

Крім того, ви можете помилитися в одному: чистота функції - це не питання плюсів і мінусів. Навіть одного сумніву, що це може мати побічний ефект, достатньо, щоб зробити його не чистим.

Ерік Ліпперт піднімає хороший момент. Я буду використовувати http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx як частину мого контр-аргументу. Особливо лінія

Чистим методом дозволяється змінювати об'єкти, створені після вступу в чистий метод.

Скажімо, ми створюємо такий метод:

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

По-перше, це передбачає, що GetEnumeratorце теж чисто (я не можу реально знайти жодного джерела про це). Якщо це так, то відповідно до вищезазначеного правила, ми можемо анотувати цей метод за допомогою [Pure], оскільки він лише модифікує екземпляр, створений в самому тілі. Після цього ми можемо скласти це і те ApplyIterator, що повинно призвести до чистої функції, правда?

Count(ApplyIterator(source, action));

Ні. Ця композиція не є чистою, навіть коли обидва Countі ApplyIteratorчисті. Але я можу будувати цей аргумент на неправильній передумові. Я думаю, що думка про те, що екземпляри, створені в рамках методу, звільнені від правила чистоти, або неправильна, або, принаймні, недостатньо конкретна.


1
Чистота функції +1 - це не питання плюсів і мінусів. Чистота функції - це натяк на використання та безпеку. Як не дивно, що ОП вводиться where T : class, однак, якщо ОП просто поставить where T : strutйого, БУДЕ бути чистим.
АрТ

4
Я не згоден з цією відповіддю. Виклик sequence.Apply(action)не має побічних ефектів; якщо це так, вкажіть побічний ефект, який він має. Тепер дзвінок sequence.Apply(action).GetEnumerator().MoveNext()має побічний ефект, але ми вже знали це; це мутує нумератор! Чому слід sequence.Apply(action)вважати нечистим, оскільки покликання MoveNextнечисте, а sequence.Where(predicate)вважати чистим? sequence.Where(predicate).GetEnumerator().MoveNext()є кожен шматочок як нечистий.
Ерік Ліпперт

@EricLippert Ви піднімаєте хорошу точку. Але, чи не буде достатньо просто зателефонувати GetEnumerator? Чи можемо ми вважати це Чистим?
Ейфорія

@Euphoric: Який помітний побічний ефект викликає виклик GetEnumerator, окрім виділення перелічувача у його початковий стан?
Ерік Ліпперт

1
@EricLippert Тоді чому це так, що Enumerable.Count вважається чистою за кодовими контрактами .NET? Я не маю посилання, але коли я граю з ним у візуальній студії, я отримую попередження, коли використовую користувальницькі нечисті рахунки, але контракт працює чудово з Enumerable.Count.
Ейфорія

18

Я не згоден з відповідями Ейфорика та Роберта Харві . Абсолютно це чиста функція; проблема в тому

Він просто застосовує дію до кожного елемента послідовності перед поверненням.

дуже незрозуміло, що означає перше "це". Якщо "це" означає одну з цих функцій, то це неправильно; жодна з цих функцій не робить цього; MoveNextз енумератора послідовності робить це, і вона «повертає» деталь через Currentвласність, не повертаючи його.

Ці послідовності перераховуються ліниво , не жадібно, тому, безумовно, не так, що дія буде застосована до повернення послідовності Apply. Дія застосовується після повернення послідовності, якщо MoveNextвикликається в перерахунку.

Як зазначаєте, ці функції виконують дію та послідовність і повертають послідовність; вихід залежить від вводу, і побічні ефекти не виробляються, тому це чисті функції ..

Тепер, якщо ви створили нумератор отриманої послідовності, а потім викликаєте MoveNext на цьому ітераторі, то метод MoveNext не є чистим, оскільки він викликає дію та створює побічний ефект. Але ми вже знали, що MoveNext не є чистим, оскільки він мутує нумератор!

Тепер, що стосується вашого питання, чи слід застосовувати атрибут: я б не застосував атрибут, тому що я не писав би цей метод в першу чергу . Якщо я хочу застосувати дію до послідовності, то я пишу

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

що добре зрозуміло.


2
Я здогадуюсь, що цей метод потрапляє в той самий мішок, що і ForEachметод подовження, який навмисно не є частиною Linq, оскільки його мета - викликати побічні ефекти ...
Томас Левеск

1
@ThomasLevesque: Моя порада ніколи цього не робити . Запит повинен відповідати на запитання , а не мутувати послідовність ; тому їх називають запитами . Мутувати послідовність під час запиту надзвичайно небезпечно . Розглянемо для прикладу, що трапляється, якщо такий запит буде піддано численним викликам Any()протягом часу; дія буде виконуватися знову і знову, але лише на першому пункті! Послідовність повинна бути послідовністю значень ; якщо ви хочете послідовність дій, тоді зробіть IEnumerable<Action>.
Ерік Ліпперт

2
Ця відповідь замулює води більше, ніж висвітлює. Хоча все, що ви говорите, є безперечно правдою, принципи незмінності та чистоти - це принципи мови мовлення програмування високого рівня, а не деталі деталізації щодо реалізації. Програмістів, що працюють на функціональному рівні, цікавить, як поводиться їх код на функціональному рівні, не чи є його внутрішня робота чистим. Вони майже напевно не чисті під капотом, якщо ви ходите досить низько. Ми загалом керуємо цими речами на архітектурі Фон Ноймана, яка, звичайно, не є чистою.
Роберт Харві

2
@ThomasEding: метод не викликає action, тому чистота не actionмає значення. Я знаю, що це виглядає так, як це називає action, але цей метод є синтаксичним цукром для двох методів, одного, який повертає нумератор, і той, який є MoveNextперелічувачем. Перший явно чистий, а другий явно ні. Подивіться на це так: ви б сказали, що IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }це чисто? Тому що це функція, яка це насправді.
Ерік Ліпперт

1
@ThomasEding: Вам щось не вистачає; це не так, як працюють ітератори. ApplyIteratorМетод повертає негайно . Жоден код в тілі ApplyIteratorне запускається до першого виклику MoveNextв обчислювачі повернутого об'єкта. Тепер, коли ви це знаєте, ви можете вивести відповідь на цю загадку: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… Відповідь тут: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Ерік Ліпперт

3

Це не є чистою функцією, тому застосування атрибута Pure вводить в оману.

Чисті функції не змінюють оригінальну колекцію, і не має значення, чи передаєте ви дію, яка не має ефекту чи ні; це все ще нечиста функція, оскільки її мета - викликати побічні ефекти.

Якщо ви хочете зробити функцію чистою, скопіюйте колекцію в нову колекцію, застосуйте зміни, які вводить Дія до нової колекції, і поверніть нову колекцію, залишивши оригінальну колекцію незмінною.


Ну, це не змінює оригінальну колекцію, оскільки вона просто повертає нову послідовність з тими ж елементами; саме тому я розглядав можливість зробити це чистим. Але це може змінити стан елементів, коли ви перераховуєте результат.
Томас Левеск

Якщо itemце посилальний тип, він змінює оригінальну колекцію, навіть якщо ви повертаєтесь itemв ітераторі. Дивіться stackoverflow.com/questions/1538301
Роберт Харві

1
Навіть якщо він глибоко скопіював колекцію, вона все одно не буде чистою, оскільки це actionможе мати інші побічні ефекти, крім модифікації переданого їй предмета.
Ідан Ар'є

@IdanArye: Щоправда, Дія також повинна бути чистою.
Роберт Харві

1
@IdanArye: ()=>{}конвертована в Action, і це чиста функція. Вихідні дані залежать виключно від його входів, і це не має помітних побічних ефектів.
Ерік Ліпперт

0

На мою думку, той факт, що він отримує Дію (а не щось на зразок PureAction), робить її не чистою.

І я навіть не згоден з Еріком Ліппертом. Він написав це "() => {} конвертоване в Action, і це чиста функція. Виходи залежать виключно від його входів, і він не має помітних побічних ефектів".

Добре, уявіть, що замість використання делегата ApplicationIterator викликав метод з назвою Action.

Якщо дія чиста, то і ApplicationIterator чистий. Якщо дія не є чистою, то ApplicationIterator не може бути чистим.

Враховуючи тип делегата (не фактичне задане значення), ми не маємо гарантії, що він буде чистим, тому метод буде вести себе як чистий метод лише тоді, коли делегат чистий. Отже, щоб зробити його справді чистим, він повинен отримати чистого делегата (і що існує, ми можемо оголосити делегата як [Pure], щоб ми могли мати PureAction).

Пояснюючи це по-іншому, метод «Чистий» повинен завжди давати однаковий результат із однаковими входами і не повинен створювати помітних змін. ApplyIterator може бути наданий одному і тому ж джерелу і делегувати двічі, але, якщо делегат змінює тип посилання, наступне виконання дасть різні результати. Приклад: Делегат робить щось на зразок item.Content + = "Змінено";

Отже, використовуючи ApplyIterator над списком "контейнерів рядків" (об'єкт із властивістю Content типу рядка), ми можемо мати ці вихідні значення:

Test

Test2

Після першого виконання список матиме таке:

Test Changed

Test2 Changed

І це втретє:

Test Changed Changed

Test2 Changed Changed

Отже, ми змінюємо вміст списку, тому що делегат не є чистим і не може бути здійснено оптимізацію, щоб уникнути виконання виклику 3 рази, якщо викликати його 3 рази, оскільки кожне виконання призведе до іншого результату.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.