четверг, 7 января 2010 г.

Каррирование в C#

Под термином каррирование понимается преобразование функции, которое функцию от двух переменных переводит в функцию от одной переменной.
Предположим, что у нас была функция:
   f:  AxB  =>   C
   f: (a,b) -> a + b

Сложение взято для примера. Мы можем её преобразовать следующим образом:
   Curry(f): A => (B =>  C  )
   Curry(f): a -> (b ->a + c)

То есть теперь каждому числу Curry(f) сопоставляет функцию одной переменной. При этом выполняется равенство
   f(a,b) = Curry(f)(a)(b)
Кроме того, при помощи функции каррирования мы можем фиксировать значение первого аргумента функции f.
   AddOne   = Curry( + )(1)
   PowerOf2 = Curry( ^ )(2)

Теперь можно использовать новополученные функции. Например:
   AddOne(5)   = 6
   PowerOf2(3) = 8

Подобные функции дают некоторую гибкость, и могут быть использованы, например при использовании методов Linq.
К сожалению, в третьей версии c# нельзя использовать вот такие конструкции:
internal static class Program
{
   public static int AddFunction(int a, int b)
   {
      return a + b;
   }

   private static void Main()
   {
      var addTwo = FunctionalTools.Curry(AddFunction)(2)
      var addTwoExtention = AddFunction.Curry()(2)
   }
}
Поскольку AddFunction на самом деле является "Method Group", и особенно в случае если Curry перегружен, не может правильно определить сигнатуру метода. Поэтому придётся использовать ручное приведение типов. Вот так:
internal static class Program
{
  public static int AddFunction(int a, int b)
  {
    return a + b;
  }

  private static void Main()
  {
    var addTwo = FunctionalTools.Curry((Func<int, int, int>)AddFunction)(2)
    var addTwoExtention = ((Func<int, int, int>)AddFunction).Curry()(2)    
  }
}
Практически всё уже сказано, осталось только реализовать функционал. К сожалению, сделать самый общий тип мы не сможем, но мы можем сделать покрытие значительной части
функционала. Скажи, вы часто используете методы, которые содержат более, чем 8 параметров? Думаю, довольно редко. Поэтому вначале обьявим несколько делегатов.
public delegate TResult Func<T1, T2, T3, T4, T5, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
public delegate TResult Func<T1, T2, T3, T4, T5, T6, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
public delegate TResult Func<T1, T2, T3, T4, T5, T6, T7, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
public delegate TResult Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);

Но обьявлять таким образом кучу делегатов скучно, долго, неинтересно и не информативно. Более того, если попытки написать функцию Curry для каждого возможного варианта не вызывают никакой радости.
Поэтому рекомендую посмотреть в сторону Text Template Transformation Toolkit, про него на хабре уже писали.

Немножечко перепишем объявление делегатов:
<# for(var index = Shapr3FuncCount; index < MaxParametersCount; index++) { #>
    public delegate TResult Func<<#= BuildTemplate(1,index) #>, TResult>(<#= BuildTemplate(1,index,x=>TypeNamePrefix+x+ArgPrefix+x) #>);
<#} #>
При этом были использованы следующие константы и функции:

   private const int Shapr3FuncCount = 5;
   private const int MaxParametersCount = 9;
   private const string TypeNamePrefix = "T";
   private const string ArgPrefix = " arg";
   private string BuildTemplate(int a,int b,Func<int,string> func){
      return string.Join(", ",System.Linq.Enumerable.Range(a, b).Select(func).ToArray());
   }
   private string BuildTemplate(int a,int b){
      return BuildTemplate(a, b, x => TypeNamePrefix + x);
   }
   private string BuildArgs(int howMany){
      return BuildTemplate(0,howMany,x => ((char)('b'+x)).ToString());
   }
Вот мы сгенерировали недостающие делегаты, теперь можно смело приступить непосредственно к написанию функций каррирования.
Всё действие будет происходить внутри конструкции:
<# for(var index = MinParametersCount; index < MaxParametersCount; index++) { #>
Собственно, ничего сложного в написании функции нет. Вот она:
public static Func<T0, Func<<#= BuildTemplate(1,index-1) #>>> Curry<<#= BuildTemplate(0,index) #>>(this Func<<#= BuildTemplate(0,index) #>> func)
{
   return a => (<#= BuildArgs(index - 2) #>) => func(a<#= index>MinParametersCount?", ":"" #><#= BuildArgs(index - 2) #>);
}

А к ней я решил добавить ещё одну функцию, которая позволит сразу зафиксировать первый аргумент какой-либо функции. Мне кажется, что это является одной из наиболее востребованных возможностей каррирования.
public static Func<<#= BuildTemplate(1,index-1) #>> Bind<<#= BuildTemplate(0,index) #>>(this Func<<#= BuildTemplate(0,index) #>> func, T0 value)
{
   return Curry(func)(value);
}

Теперь осталось только сохранится и посмотреть получивший файл. Изменяя константы можно плодить дополнительные функции, которые удовлетворит любые потребности в аргументах, хотя и сомневаюсь, что такие могут реально возникнуть.
Для удобства можно реализовать ещё несколько функций высших порядков. Я приведу лишь одну, которая позволит менять местами аргументы функции:
public static Func Flip<T1,T2,TResult>(this Func<T1,T2,TResult> func)
{
   return (x,y) => func(y,x);
}

За этим рассказ окончен, хочу лишь ещё раз обратить внимание что различные функциональные практики постепенно входят в жизнь обыкновенного программиста, что немного радует.
Шаблон и получившийся в результате кодогенерации файл можно скачать.