Этот метод чистый?

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, вероятно, вводит в заблуждение ...

Что вы думаете? Должен ли я применить атрибут или нет?

Томас Левеск
источник
Связанный: stackoverflow.com/questions/23997554/…
Ден

Ответы:

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чиста. Но я, возможно, строю этот аргумент на неправильной предпосылке. Я думаю, что идея о том, что экземпляры, созданные в методе, освобождены от правила чистоты, либо неверна, либо, по крайней мере, недостаточно конкретна.

Euphoric
источник
1
+1 Чистота функции - это не вопрос плюсов или минусов. Чистота функции - это подсказка по использованию и безопасности. Достаточно странно, что OP вставил where T : class, однако, если OP просто выразился, 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? У меня нет ссылки, но когда я играю с ней в visual studio, я получаю предупреждение, когда использую нестандартный не чистый счет, но контракт отлично работает с 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, и это чистая функция. Его выходные данные зависят только от его входных данных и не имеют видимых побочных эффектов".

Хорошо, представьте, что вместо использования делегата ApplyIterator вызывал метод с именем Action.

Если Action является чистым, то ApplyIterator также является чистым. Если действие не является чистым, то ApplyIterator не может быть чистым.

Учитывая тип делегата (не фактическое заданное значение), у нас нет гарантии, что он будет чистым, поэтому метод будет вести себя как чистый метод только тогда, когда делегат чистый. Итак, чтобы сделать его действительно чистым, он должен получить чистый делегат (и он существует, мы можем объявить делегат как [Pure], чтобы мы могли иметь PureAction).

Объясняя это по-разному, метод Pure всегда должен давать один и тот же результат при одинаковых входных данных и не должен генерировать наблюдаемых изменений. ApplyIterator может быть предоставлен один и тот же источник и делегат дважды, но, если делегат меняет ссылочный тип, следующее выполнение даст другие результаты. Пример: делегат делает что-то вроде item.Content + = "Changed";

Итак, используя ApplyIterator над списком «строковых контейнеров» (объект со свойством Content типа string), мы можем получить следующие исходные значения:

Test

Test2

После первого выполнения список будет иметь это:

Test Changed

Test2 Changed

И это в третий раз:

Test Changed Changed

Test2 Changed Changed

Таким образом, мы меняем содержимое списка, потому что делегат не является чистым, и никакая оптимизация не может быть выполнена, чтобы избежать выполнения вызова 3 раза, если вызов вызван 3 раза, поскольку каждое выполнение будет генерировать различный результат.

Пауло Земек
источник