РеFUNкторинг, генерики и HOF

Рассмотрим простой пример на C#. Допустим, на вход подаётся список чисел, и мы должны вернуть список строк, каждая из которых — это исходное число, увеличенное на единичку, с префиксом «N «.

 

Соответствующую функцию можно записать так:

  static List<string> addOne(List<int> list) {
  List<string> str_lst = new List<string>();
  list.ForEach(delegate(int val)
    {
     string s = "N " + (val + 1);
     str_lst.Add(s);
    });
    return str_lst;
   }

К этому коду несложно прикрутить тесты — например, на вход подаём {0,1,2} и на выходе получаем {«N 1», «N 2», «N 3»}, и на этом успокоиться.

 

Но вот заказчик просит, чтобы вместо префикса «N » выдавался бы префикс «# «. Конечно, продвинутый миддл не будет ни в коем случае ничего копипастить. Он бессознательно выполнит рефанкторинг (re-functoring, преобразование функций): добавит в функцию новый строковый параметр, и параметризует ту часть кода, которая теперь зависит от внешних требований:

 

  static List<string> addOne(List<int> list, string prefix) {
  List<string> str_lst = new List<string>();
  list.ForEach(delegate(int val)
    {
     string s = prefix + (val + 1);
     str_lst.Add(s);
    });
    return str_lst;
   }

 

При этом сломаются тесты, и их тоже надо будет подправить.

Вроде бы всё правильно и хорошо.

 

Но вот заказчик просит выводить префикс не как префикс, а как постфикс — не в начале строки, а в конце.

Ok, миддл выполняет очередной рефанкторинг — добавляет новый булев параметр, который определяет положение префикса-постфикса:

 

  static List<string> addOne(List<int> list, string prefix, bool post) {
  List<string> str_lst = new List<string>();
  list.ForEach(delegate(int val)
    {

     string s;
     if(post) s = (val + 1) + prefix;    
     else s = prefix + (val + 1);
     str_lst.Add(s);
    });
    return str_lst;
   }

Теперь заказчик хочет новую фичу: подавать на вход список строк, и возвращать список чисел, некоторым способом получаемый из строк.

 

Хороший разработчик поступит чуть хитрее: он вынесет функцию преобразования строки в число вовне, и будет передавать её как параметр:

 

  static List<int> fromStr(List<string> list, Func<string, int> conv) {
  List<int>int_lst = new List<int>();
  list.ForEach(delegate(string val)
    {
     int_lst.Add(conv(val));
    });
    return int_lst;
   }

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

 

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

 

static List<U> FromTo<T, U>( Func<T, U> conv, List<T> list) {
  List<U> result = new List<U>();

  list.ForEach(delegate(T val)
    {
     result.Add(conv(val));
    });
  return result;
}

Как же поступит функциональный программист? Он наверняка увидит в требованиях легендарную функцию map(), которая в C# называется select (в Linq):

 

static List<U> FromTo<T, U>(Func<T, U> conv, List<T> list) {
    IEnumerable<U> s = from i in list select conv(i);
    return s.ToList();
}

Или даже более прагматично, если шаблоны пока не требуются — в стиле, близком к лямбда-исчислениям:

 

  static List<string> fromStr2(List<int> list) {
   Func<int, string> conv = n => ("N "+n.ToString());

   IEnumerable<string> s = from i in list select conv(i);
   return s.ToList();
   }

Точнее, если мы полагаем, что наш тип Func<T, U> вполне способен

преобразовать всё, что может потребоваться в проекте, то лучше обойтись первым подходом (map и функции высших порядков). Если же нам более важно скрыть детали имплементации функции convert, лучше воспользоваться вторым подходом.

 

А насколько это всё сложно? и насколько это стимулирует к использованию функциональных подходов?

 

Отмечу, что вот такие вкусности, которые пожалуй вершина возможностей C# (или Java) — это лишь самый бейсик, самая основа функциональных языков. Например, функция map() появилась лишь в Java 8, и те мощные фичи, которые добавятся в эти языки ещё лет через 10, давным давно массово используются в функциональных платформах.

 

Я далеко не первый, кто предлагает переходить от концепции рефакторинга к концепции рефанкторинга — когда вместо непонятной и субьективной модификации кода (улучшайзинга) мы сознательно и целенаправленно занимаемся генерализацией функций и проработкой системы типов. Рефакторинг сегодня жутко замусоленный термин, что только под ним не понимают. Хотя, на самом деле, мы можем полностью уничтожить понятие «рефакторинг» одним лишь упоминанием слов «функциональное программирование».

 

На самом деле рефанкторингом занимается достаточно много программистов, просто неосознанно. Они переделывают и переделывают свой код на не слишком мощных языках с единственной целью — сымитировать мощность, которая уже давно доступна на языках функционального и мета-программирования. Например, они интуитивно собирают функции высшего порядка с параметрами полиморфного типа (что важно, ковариантные — чтобы после преобразования сохранялась структура наследования и для перечисления IEnumerable<U>), выполняют рефакторинг аргументов, заменяя их на методы и не подозревая, что это на самом деле частичное применение, и т. д. и т. п.

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

Поделиться статьей ...Share on Facebook0Share on Google+0Tweet about this on TwitterShare on LinkedIn0Share on VKPrint this page

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *