2016-12-26 2 views
1

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

  • Установите каждую переменную в каждом классе, как readonly и присвоить ему значение только один раз.
  • Использовать непреложные коллекции
  • Не пишите код с побочными эффектами.

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

Вещь, я не знаю, как вообще подходить к проблеме. Это то, что я только что описал, даже правильный способ думать об этом? Мой код до сих пор является полузапеченным, неполным беспорядком. Можно ли здесь использовать лямбду? Это поможет, или есть лучший способ?

FileObject класс просто содержит string fileName и int id, а конструктор FileObject просто и наивно создает экземпляр с учетом этих двух значений.

public IEnumerable<FileObject> EnumerateImagesInPath(string folderPath, int firstAssignedID) 
    { 
     foreach (string path in Directory.EnumerateFiles(folderPath) 
     { 
      yield return new FileObject(Path.GetFileName(imagePath) ,); 
     } 
    } 
+0

Что именно вы подразумеваете под «функциональным дизайном»? Я имею в виду разные вещи для разных людей. – JuanR

+0

_ «функциональные принципы» _ - Я думаю, вы имеете в виду _best practice_ – MickyD

+0

Я учусь, у меня нет четкого представления о том, что такое функциональное программирование, но мне обещали, что это полезная вещь, на которую нужно смотреть. До сих пор я рассматриваю это как набор ограничений, которые вы накладываете на способ написания кода, который позволяет четко определять области и отслеживать состояние. Функции небольшие и сфокусированы. Вы можете быть уверены, что ваши функции не изменяются больше, чем кажется. Переменная будет содержать только одно значение, которое было назначено ему на начальном этапе, поэтому порядок, в который вы положили утверждения, очевиден - если вы должны были поставить их в неправильном порядке, это приведет к ошибке компилятора. –

ответ

1

Самый функциональный способ делать то, что вы хотите, это:

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.

+0

Спасибо за ответ. я отвечаю в глубину прямо сейчас, потому что я на работе, но вчера я попробовал ваше первое (более простое) решение, и он работал нормально. Я думаю, что этот способ более функциональный, чем у Дэниела - одна из веских причин - это устранение переменной итератора, ваш ответ вводит функции (Range, Zip), которые кажутся вам идеально подходящими для работы. Я не знал, что вы можете создать не анонимную функцию с помощью лямбда-оператора. –

+0

Возможно, использование термина «более функциональный» бессмысленно. решение соответствует моим идеалам лучше. –

+0

Я знаю, что вы имеете в виду :) Функциональный для меня означает: 1. Использование функции первого класса s 2. По возможности все чистое выражение 3. Функции/методы являются ссылочно прозрачными (без побочных эффектов) 4. Структуры данных неизменяемы Помимо 1, остальные являются необязательными на большинстве функциональных языков. Но мне нравится рассматривать эту лучшую практику. Вы делаете все правильно, пытаясь развить свою голову в функциональном программировании, это даст вам сверхспособности в качестве кодера. Я лично хотел, чтобы мои первые 20 лет, когда программист не был в мире императива. Лучше поздно, чем никогда! – louthster

1

Использование получали методом кажется ненужным:

public IEnumerable<FileObject> EnumerateImagesInPath(string folderPath, int firstAssignedID) 
    { 
     foreach (FileObject File in Directory.EnumerateFiles(folderPath) 
      .Select(FileName => new FileObject(FileName, firstAssignedID++))) 
     { 
      yield return File; 
     } 
    } 
+0

Это работает! Я собираюсь изучить его внимательно. Несколько вопросов, хотя, если вы не возражаете, прежде чем я отмечу это как ответ: что вы имеете в виду, использование урожая кажется ненужным? Есть ли другой способ сделать это? Кроме того, используется ли ваш оператор ++ как побочный эффект или что-то плохое, поскольку он многократно меняет значение firstAssignedID? –

+0

Также, насколько я могу судить, это перечисляет все файлы в каталоге и сохраняет их в списке, прежде чем уступить первое, что не было целью - один за другим было бы более идеальным. Но я посмотрю, смогу ли я адаптировать свой ответ. –

+0

Я изменил свой ответ для ваших нужд, но использование урожая кажется ненужным, так как EnumerateFiles не собирается делать какой-либо Lazy Evaluation: D –

Смежные вопросы