Как заменить циклы for функциональным оператором в C #? - PullRequest
31 голосов
/ 15 апреля 2010

Коллега однажды сказал, что Бог убивает котенка каждый раз, когда я пишу цикл.

Когда его спросили, как избежать циклов for, он ответил, что использует функциональный язык. Однако, если вы застряли на нефункциональном языке, скажем C #, какие существуют методы, чтобы избежать циклов for или избавиться от них путем рефакторинга? С лямбда-выражениями и LINQ возможно? Если да, то как?

Вопросы

Итак, вопрос сводится к:

  1. Почему плохие циклы for? Или в каком контексте избегать циклов for и почему?
  2. Можете ли вы привести примеры кода на C #, как он выглядит раньше, то есть с циклом, а затем без цикла?

Ответы [ 18 ]

29 голосов
/ 15 апреля 2010

Функциональные конструкции часто выражают ваше намерение более четко, чем циклы for, в тех случаях, когда вы оперируете с некоторым набором данных и хотите преобразовать, отфильтровать или объединить элементы.

Циклы очень удобны, если вы хотите несколько раз выполнить какое-либо действие.


Например

int x = array.Sum();

гораздо более ясно выражает ваше намерение, чем

int x = 0;
for (int i = 0; i < array.Length; i++)
{
    x += array[i];
}
16 голосов
/ 15 апреля 2010

Почему плохие циклы for? Или в чем контекст для циклов, чтобы избежать и почему?

Если у вашего коллеги есть функциональное программирование, то он, вероятно, уже знаком с основными причинами избегания циклов:

Fold / Map / Filter охватывает большинство случаев использования обхода списка и хорошо подходит для составления функций. Циклы for не являются хорошим шаблоном, потому что они не поддаются компоновке.

В большинстве случаев вы просматриваете список, чтобы сложить (агрегировать), отобразить или отфильтровать значения в списке. Эти функции более высокого порядка уже существуют в каждом основном функциональном языке, поэтому вы редко видите идиому цикла for, используемую в функциональном коде.

Функции высшего порядка - это набор функций, то есть вы можете легко объединить простую функцию в нечто более сложное.

Чтобы привести нетривиальный пример, рассмотрим следующее на императивном языке:

let x = someList;
y = []
for x' in x
    y.Add(f x')

z = []
for y' in y
    z.Add(g y')

На функциональном языке мы написали бы map g (map f x), или мы можем исключить промежуточный список, используя map (f . g) x. Теперь мы можем, в принципе, исключить промежуточный список из обязательной версии, и это помогло бы немного, но не сильно.

Основная проблема с императивной версией заключается в том, что циклы for являются подробностями реализации . Если вы хотите изменить функцию, вы измените ее реализацию - и в итоге вы изменили много кода.

В качестве примера, как бы вы написали map g (filter f x) в обязательном порядке? Ну, поскольку вы не можете повторно использовать свой исходный код, который отображает и отображает, вам нужно написать новую функцию, которая фильтрует и отображает вместо этого. И если у вас есть 50 способов отображения и 50 способов фильтрации, то, как вам нужно 50 ^ 50 функций, или вам нужно смоделировать возможность передавать функции в качестве первоклассных параметров, используя шаблон команды (если вы когда-либо пробовали функциональное программирование в Java вы понимаете, какой это может быть кошмар).

Вернувшись в функциональную вселенную, вы можете обобщить map g (map f x) таким образом, чтобы вы могли заменять map на filter или fold при необходимости:

let apply2 a g b f x = a g (b f x)

И назовите его, используя apply2 map g filter f или apply2 map g map f или apply2 filter g filter f, или как вам нужно. Теперь вы, вероятно, никогда не будете писать подобный код в реальном мире, вы, вероятно, упростите его, используя:

let mapmap g f = apply2 map g map f
let mapfilter g f = apply2 map g filter f

Функции высшего порядка и состав функций дают вам уровень абстракции, который вы не можете получить с помощью императивного кода.

Абстрагирование деталей реализации циклов позволяет плавно менять один цикл на другой.

Помните, for-циклы - это детали реализации. Если вам нужно изменить реализацию, вам нужно изменить каждый цикл for.

Карта / сворачивание / фильтрация абстрагирования от петли. Поэтому, если вы хотите изменить реализацию ваших циклов, вы измените их в этих функциях.

Теперь вы можете задаться вопросом, почему вы хотите абстрагироваться от цикла. Рассмотрим задачу отображения элементов из одного типа в другой: обычно , элементы отображаются по одному, последовательно и независимо от всех других элементов. В большинстве случаев подобные карты являются основными кандидатами для распараллеливания.

К сожалению, детали реализации для последовательных карт и параллельных карт не являются взаимозаменяемыми. Если у вас есть тонна последовательных карт по всему коду, и вы хотите заменить их на параллельные карты, у вас есть два варианта: копировать / вставлять один и тот же код параллельного отображения по всей вашей кодовой базе или абстрагировать логику удаленного отображения в две функции map и pmap. Пройдя второй путь, вы уже по колено на территории функционального программирования.

Если вы понимаете назначение композиции функций и абстрагирование деталей реализации (даже таких деталей, как зацикливание), вы можете начать понимать, как и почему функциональное программирование настолько мощно, во-первых.

14 голосов
/ 15 апреля 2010
  1. Для петель неплохо. Есть много очень веских причин держать цикл for.
  2. Часто можно избежать цикла for, переработав его с помощью LINQ в C #, что обеспечивает более декларативный синтаксис. Это может быть хорошо или плохо в зависимости от ситуации:

Сравните следующее:

var collection = GetMyCollection();
for(int i=0;i<collection.Count;++i)
{
     if(collection[i].MyValue == someValue)
          return collection[i];
}

против foreach:

var collection = GetMyCollection();
foreach(var item in collection)
{
     if(item.MyValue == someValue)
          return item;
}

против. LINQ:

var collection = GetMyCollection();
return collection.FirstOrDefault(item => item.MyValue == someValue);

Лично у всех трех вариантов есть свое место, и я использую их все. Это вопрос использования наиболее подходящего варианта для вашего сценария.

14 голосов
/ 15 апреля 2010

В циклах нет ничего плохого, но вот некоторые причины, по которым люди могут предпочесть функциональные / декларативные подходы, такие как LINQ, где вы объявляете то, что хотите, а не то, как вы это получаете: -

  1. Функциональные подходы потенциально легче распараллелить либо вручную, используя PLINQ, либо компилятором. Поскольку процессоры переходят на еще большее количество ядер, это может стать более важным.

  2. Функциональные подходы облегчают достижение отложенной оценки в многоэтапных процессах, потому что вы можете передать промежуточные результаты на следующий шаг в виде простой переменной, которая еще не была оценена полностью, а не оценивать первый шаг полностью и затем передача коллекции на следующий шаг (или без использования отдельного метода и оператора yield для достижения того же самого процедурного значения).

  3. Функциональные подходы часто короче и проще для чтения.

  4. Функциональные подходы часто устраняют сложные условные тела внутри циклов for (например, операторы if и операторы continue), потому что вы можете разбить цикл for на логические шаги - выбрать все соответствующие элементы, выполнить над ними операцию , ...

8 голосов
/ 15 апреля 2010

За петли не убивайте людей (или котят, или щенков, или трибблов). Люди убивают людей. Ибо петли сами по себе неплохие. Однако, как и все остальное, то, как вы их используете, может быть плохо.

6 голосов
/ 16 апреля 2010

Иногда вы не убиваете только одного котенка.

для (int i = 0; i

Иногда вы их всех убиваете.

5 голосов
/ 15 апреля 2010

Вы можете рефакторинг своего кода достаточно хорошо, чтобы вы не видели их часто. Хорошее имя функции определенно более читабельно, чем цикл for.

Пример из AndyC:

Loop

// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
{
    if(s.Length > 5)
    {
        myList.add(s);
    }
}

Linq

// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5)
                               .ToList<string();

Когда вы используете первую или вторую версию внутри вашей функции, ее легче читать

var filteredList = myList.GetStringLongerThan(5);

Это слишком простой пример, но вы меня поняли.

3 голосов
/ 15 апреля 2010

Ваш коллега ошибается из-за того, что во всех случаях циклы плохие, но исправьте, что их можно переписать функционально.

Скажем, у вас есть метод расширения, который выглядит следующим образом:

void ForEach<T>(this IEnumerable<T> collection, Action <T> action)
{
    foreach(T item in collection)
    {
        action(item)
    }
}

Тогда вы можете написать цикл следующим образом:

mycollection.ForEach(x => x.DoStuff());

Это может быть не очень полезно сейчас. Но если вы замените свою реализацию метода расширения ForEach на использование многопоточного подхода, тогда вы получите преимущества параллелизма.

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

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

3 голосов
/ 15 апреля 2010

Ваш коллега не прав. Для петель не так уж плохо. Они чистые, читаемые и не подвержены ошибкам.

2 голосов
/ 15 апреля 2010

Простой (и действительно бессмысленный) пример:

Петля

// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
{
    if(s.Length > 5)
    {
        myList.add(s);
    }
}

Linq

// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5).ToList<string>();

В моей книге вторая выглядит намного аккуратнее и проще, хотя с первой нет ничего плохого.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...