Является ли этот метод расширения С# нечистым и, если да, то плохим кодом?

Я немного изучаю программирование функций, и мне интересно:

1) Чист ли мой метод расширения ForEach? То, как я это называю, кажется, нарушает правило «не связываться с передаваемым объектом», верно?

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
  foreach ( var item in source )
     action(item);
}


static void Main(string[] args)
{
    List<Cat> cats = new List<Cat>()
    {
        new Cat{ Purring=true,Name="Marcus",Age=10},
        new Cat{ Purring=false, Name="Fuzzbucket",Age=25 },
        new Cat{ Purring=false, Name="Beanhead",Age=9 },
        new Cat{Purring=true,Name="Doofus",Age=3}
    };


    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });

    // *************************************************
    //  Does this code make the extension method impure?
    // *************************************************
    cats.Where(x => x.Purring == false).ForEach(x =>
    {
        x.Purring = true; // purr,baby
    });

    // all the cats now purr
    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });
}

public class Cat {
        public bool Purring;
        public string Name;
        public int Age;
}

2) Если это нечисто, это плохой код? Лично я считаю, что код выглядит чище, чем старый foreach ( var item in items) { blah; }, но я беспокоюсь, что, поскольку он может быть нечистым, он может создать беспорядок.

3) Будет ли плохой код, если он вернет IEnumerable<T> вместо void? Я бы сказал, что пока он нечист, да, это будет очень плохой код, поскольку он будет поощрять создание цепочки чего-то, что изменит цепочку. Например, это плохой код?

// possibly bad extension
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach ( var item in source )
        action(item);

    return source;

}

person Community    schedule 28.03.2009    source источник


Ответы (4)


Нечистота не обязательно означает плохой код. Многие люди считают простым и полезным использовать побочные эффекты для решения проблемы. Ключ в том, чтобы сначала знать, как это сделать чистым способом, чтобы вы знали, когда нечистота уместна :).

В .NET нет концепции чистоты в системе типов, поэтому «чистый» метод, который принимает произвольные делегаты, всегда может быть нечистым, в зависимости от того, как он вызывается. Например, «Где», также известная как «фильтр», обычно считается чистой функцией, поскольку она не изменяет свои аргументы и не изменяет глобальное состояние.

Но ничто не мешает вам поместить такой код в аргумент Where. Например:

things.Where(x => { Console.WriteLine("um?"); 
                    return true; })
      .Count();

Так что это определенно нечистое использование Where. Enumerables могут делать все, что захотят, во время итерации.

Плохой ли у вас код? Нет. Использование цикла foreach так же "нечисто" - вы по-прежнему изменяете исходные объекты. Я постоянно пишу такой код. Соедините вместе некоторые выборки, фильтры и т. д., а затем выполните для них ForEach, чтобы вызвать некоторую работу. Вы правы, это чище и проще.

Пример: наблюдаемаяколлекция. По какой-то причине у него нет метода AddRange. Итак, если я хочу добавить к нему кучу вещей, что мне делать?

foreach(var x in things.Where(y => y.Foo > 0)) { collection.Add(x)); } 

or

things.Where(x => x.Foo > 0).ForEach(collection.Add);

Я предпочитаю второй. Как минимум, я не понимаю, как это может быть истолковано как худшее, чем первый способ.

Когда это плохой код? Когда он создает побочный код в неожиданном месте. Это относится к моему первому примеру с использованием Where. И даже тогда бывают случаи, когда область применения очень ограничена, а использование понятно.

Связывание ForEach

Я написал код, который делает такие вещи. Чтобы избежать путаницы, я бы дал ему другое имя. Основная путаница заключается в том, «это сразу оценивается или лениво?». ForEach подразумевает, что он сразу же выполнит цикл. Но что-то, возвращающее IEnumerable, подразумевает, что элементы будут обрабатываться по мере необходимости. Поэтому я бы предложил дать ему другое имя ("Process", "ModifySeq", "OnEach"... что-то в этом роде) и сделать его ленивым:

public static IEnumerable<T> OnEach(this IEnumerable<T> src, Action<T> f) {
  foreach(var x in src) {
    f(x);
    yield return x;
  }
}
person MichaelGG    schedule 28.03.2009

Это не чисто, так как может вызывать нечистые методы. Я думаю, согласно типичным определениям, чистота — это транзитивное замыкание: функция является чистой только в том случае, если все функции, которые она вызывает (прямо или косвенно), также являются чистыми или если эффекты этих функций инкапсулированы (например, они только мутируют не экранирующую функцию). локальная переменная).

person Brian    schedule 28.03.2009
comment
По сути, любой общедоступный метод .NET, который вызывает аргумент функции, является нечистым, потому что он может вызывать нечистый код? - person MichaelGG; 29.03.2009
comment
Можно представить себе систему типов, в которой вы можете быть «обобщенным» по отношению к «чистоте», и, таким образом, G(Func‹T›f) может иметь G как чистый-когда-f-является-чистым и нечистым-когда-f- есть-нечистый. - person Brian; 29.03.2009
comment
Но пока у нас нет этого в системе типов, разве не полезно иметь возможность отличать HoF, которые являются чистыми, если их параметры не являются, от тех, которые просто не являются? - person MichaelGG; 30.03.2009
comment
Я не уверен, о чем вы спрашиваете ... пока у нас нет этого в системе типов, еще нет даже атрибута «Чистый», не говоря уже о системе типов для него; мы уже обсуждаем футуристические фантазии, так что ничего "полезного" само по себе нет, это все кабинетно-языково-дизайнерские спекуляции. - person Brian; 30.03.2009
comment
В любом случае, я только что проголосовал за ваш ответ, так как он лучше моего и более соответствует теме. Бит более высокого порядка заключается в том, чтобы вместо того, чтобы сосредотачиваться на определениях и системах типов, писать хороший код, который сводит к минимуму удивительные/неожиданные эффекты и взаимодействия. - person Brian; 30.03.2009
comment
Да, после того, как я написал это, я понял, что должен был сказать что-то вроде того, что его нет в системе типов и, вероятно, никогда не будет. Обсуждать это полезно так же, как будет полезен атрибут Pure в CodeContracts — для размышлений о коде. - person MichaelGG; 30.03.2009
comment
В частности, если бы мы могли сказать, что Where и Select в LINQ являются чистыми, то мы всегда могли бы действовать, исходя из этого предположения, и единственные люди, которые пострадают, — это те, кто использует их нечисто. - person MichaelGG; 30.03.2009

Да, это не чисто, но это спорный вопрос, так как это даже не функция.

Поскольку метод ничего не возвращает, единственная возможность для него вообще что-либо сделать — это либо повлиять на объекты, которые вы отправляете, либо повлиять на что-то несвязанное (например, запись в окно консоли).

Изменить:
Чтобы ответить на ваш третий вопрос; да, это плохой код, поскольку он, кажется, делает что-то, чего не делает. Метод возвращает коллекцию, поэтому она кажется чистой, но поскольку он просто возвращает отправленную коллекцию, на самом деле она не более чистая, чем первая версия. Чтобы иметь какой-то смысл, метод должен принимать делегата Func<T,T> для использования в качестве преобразования и возвращать коллекцию преобразованных элементов:

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Func<T,T> converter) {
   foreach (T item in source) {
      yield return converter(item);
   }
}

Конечно, все еще зависит от функции конвертера, если вызов расширения чистый. Если он не делает копию элемента ввода, а просто изменяет его и возвращает, вызов все равно не является чистым.

person Guffa    schedule 28.03.2009
comment
@Noldorin: Нет, я не это имею в виду. У него нет возвращаемого значения, поэтому это не функция. - person Guffa; 29.03.2009

Действительно, поскольку ваше лямбда-выражение содержит присваивание, функция теперь по определению нечиста. Связано ли присваивание с одним из аргументов или другим объектом, определенным вне текущей функции, не имеет значения... Функция не должна иметь никаких побочных эффектов, чтобы ее можно было назвать чистой. См. Википедию для более точного (хотя и довольно простого) определения, в котором подробно описаны два условия функции должен удовлетворять требованиям, чтобы считаться чистым (один из них — отсутствие побочных эффектов). Я считаю, что лямбда-выражения обычно предназначены для использования в качестве чистых функций (по крайней мере, я полагаю, что они изначально изучались как таковые с математической точки зрения), хотя очевидно, что C # не является строгим в этом отношении, где чисто функциональные языки. Так что это, вероятно, неплохая практика, хотя определенно стоит держаться подальше от того, что такая функция нечиста.

person Noldorin    schedule 28.03.2009