Самый функциональный способ делать то, что вы хотите, это:
IEnumerable<FileObject> EnumerateImagesInPath(string path, int firstAssignedID) =>
Enumerable.Zip(
Enumerable.Range(firstAssignedID, Int32.MaxValue),
Directory.EnumerateFiles(path),
FileObject.New);
С типом FileObject
определенным образом:
public class FileObject
{
public readonly int Id;
public readonly string Filename;
FileObject(int id, string fileName)
{
Id = id;
Filename = fileName;
}
public static FileObject New(int id, string fileName) =>
new FileObject(id, fileName);
}
Он не использует yield
, но это не имеет значения, потому что Enumerable.Range
и Enumerable.Zip
делают, поэтому это ленивая функция, как и ваш оригинальный пример.
Я использую Enumerable.Range
для создания ленивого списка целых чисел от firstAssignedId
до Int32.MaxValue
. Это заархивировано вместе с перечислимыми файлами в каталоге. FileObject.New(id. path)
вызывается как часть вычисления zip.
Не существует модификации состояния на месте, как принятый ответ (firstAssignedID++
), и вся функция может быть представлена как выражение.
Другой способ достижения вашей цели - использовать шаблон fold
. Это самый распространенный способ агрегирования состояния в функциональном программировании. Это, как определить его IEnumerable
public static class EnumerableExt
{
public static S Fold<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder) =>
self.Any()
? Fold(self.Skip(1), folder(state, self.First()), folder)
: state;
}
Вы должны быть в состоянии видеть, что его рекурсивная функция, которая запускает делегат (folder
) на голове списка, если есть один, а затем использует это в качестве нового государства при вызове рекурсивного вызова Fold
. Если он достигнет конца списка, возвращается состояние агрегата.
Вы можете заметить, что реализация EnumerableExt.Fold
может взорвать стек в C# (из-за отсутствия оптимизации хвостового вызова). Таким образом, лучший способ реализации функции Fold
это сделать так повелительно:
public static S Fold<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder)
{
foreach(var x in self)
{
state = folder(state, x);
}
return state;
}
Существует сопряженное к Fold
известный как FoldBack
(иногда их называют «складка влево» и «свернуть вправо»). FoldBack
по существу агрегаты от хвоста списка до головы, где Fold
от головы до хвоста.
public static S FoldBack<S, T>(this IEnumerable<T> self, S state, Func<S, T, S> folder)
{
foreach(var x in self.Reverse()) // Note the Reverse()
{
state = folder(state, x);
}
return state;
}
Fold настолько гибок, например, вы могли бы реализовать Count
для перечислимого с точки зрения fold
так:
int Count<T>(this IEnumerable<T> self) =>
self.Fold(0, (state, item) => state + 1);
Или Sum
так:
int Sum<int>(this IEnumerable<int> self) =>
self.Fold(0, (state, item) => state + item);
Или большинство API IEnumerable
!
public static bool Any<T>(this IEnumerable<T> self) =>
self.Fold(false, (state, item) => true);
public static bool Exists<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
self.Fold(false, (state, item) => state || predicate(item));
public static bool ForAll<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
self.Fold(true, (state, item) => state && predicate(item));
public static IEnumerable<R> Select<T, R>(this IEnumerable<T> self, Func<T, R> map) =>
self.FoldBack(Enumerable.Empty<R>(), (state, item) => map(item).Cons(state));
public static IEnumerable<T> Where<T>(this IEnumerable<T> self, Func<T, bool> predicate) =>
self.FoldBack(Enumerable.Empty<T>(), (state, item) =>
predicate(item)
? item.Cons(state)
: state);
Это очень мощный и позволяет агрегировать состояния для коллекции (так что это позволяет нам делать firstAssignedId++
без императивного государственной модификации на месте).
Наш FileObject
пример является немного более сложным, чем Count
или Sum
, потому что мы должны поддерживать две части государства: совокупный ID и полученный IEnumerable<FileObject>
. Таким образом, наше государство Tuple<int, IEnumerable<FileObject>>
IEnumerable<FileObject> FoldImagesInPath(string folderPath, int firstAssignedID) =>
Directory.EnumerateFiles(folderPath)
.Fold(
Tuple.Create(firstAssignedID, Enumerable.Empty<FileObject>()),
(state, path) => Tuple.Create(state.Item1 + 1, FileObject.New(state.Item1, path).Cons(state.Item2)))
.Item2;
Вы можете сделать это еще более декларативный, обеспечивая некоторое расширение и статические методы для Tuple<int, IEnumerable<FileObject>>
:
public static class FileObjectsState
{
// Creates a tuple with state ID of zero (Item1) and an empty FileObject enumerable (Item2)
public static readonly Tuple<int, IEnumerable<FileObject>> Zero =
Tuple.Create(0, Enumerable.Empty<FileObject>());
// Returns a new tuple with the ID (Item1) set to the supplied argument
public static Tuple<int, IEnumerable<FileObject>> SetId(this Tuple<int, IEnumerable<FileObject>> self, int id) =>
Tuple.Create(id, self.Item2);
// Returns the important part of the result, the enumerable of FileObjects
public static IEnumerable<FileObject> Result(this Tuple<int, IEnumerable<FileObject>> self) =>
self.Item2;
// Adds a new path to the aggregate state and increases the ID by one.
public static Tuple<int, IEnumerable<FileObject>> Add(this Tuple<int, IEnumerable<FileObject>> self, string path) =>
Tuple.Create(self.Item1 + 1, FileObject.New(self.Item1, path).Cons(self.Item2));
}
Методы расширения захвата операций на агрегатном состоянии и сделать в результате fold
вычисления очень ясно:
IEnumerable<FileObject> FoldImagesInPath(string folderPath, int firstAssignedID) =>
Directory.EnumerateFiles(folderPath)
.Fold(
FileObjectsState.Zero.SetId(firstAssignedID),
FileObjectsState.Add)
.Result();
Очевидно, используя Fold
для прецедента, который вы предоставили, является излишним, и именно поэтому я использовал Zip
. Но более общая проблема, с которой вы боролись (функциональное агрегированное состояние), - это то, что для Fold
.
Существует еще один метод расширения я использовал в примере выше: Cons
:
public static IEnumerable<T> Cons<T>(this T x, IEnumerable<T> xs)
{
yield return x;
foreach(var a in xs)
{
yield return a;
}
}
Более подробная информация о cons can be found here
Если вы хотите узнать больше об использовании функциональной техники в C#, please check my library: language-ext. Это даст вам массу вещей, которые отсутствуют в C# BCL.
Что именно вы подразумеваете под «функциональным дизайном»? Я имею в виду разные вещи для разных людей. – JuanR
_ «функциональные принципы» _ - Я думаю, вы имеете в виду _best practice_ – MickyD
Я учусь, у меня нет четкого представления о том, что такое функциональное программирование, но мне обещали, что это полезная вещь, на которую нужно смотреть. До сих пор я рассматриваю это как набор ограничений, которые вы накладываете на способ написания кода, который позволяет четко определять области и отслеживать состояние. Функции небольшие и сфокусированы. Вы можете быть уверены, что ваши функции не изменяются больше, чем кажется. Переменная будет содержать только одно значение, которое было назначено ему на начальном этапе, поэтому порядок, в который вы положили утверждения, очевиден - если вы должны были поставить их в неправильном порядке, это приведет к ошибке компилятора. –