代码改变世界

C#函数式编程之由函数构建函数

2015-03-17 16:30  y-z-f  阅读(2946)  评论(1编辑  收藏  举报

      在面向对象的编程中,如果我们需要复用其他的类,我们可以通过继承来实现。而在函数式编程中我们也可以采取不同的方式来复用这些函数。今天的教程将会讲述两种方式,其中一个就是组合,将多个函数组合成为一个函数,另一个则是之前我们介绍过的部分应用,当然我们将会讲述如何将其高级化,来符合我们的使用要求。

 

组合

顾名思义,组合就是将函数A的结果传递给函数B。但是我们并不关注函数A的结果,当然大多数一定会这样去做:

1 var r1 = funcA(1);
2 var r2 = funcB(r1);

 

 

这样显然不是我们希望的那样,假设我们后面需要经常利用到这样的函数。问题就出现了,所以我们就需要利用组合来将他们合成一个新的函数,首先我们先写出两个用来组合的函数:

1 public static int FuncA(int x)
2 {
3      return x + 3;
4 }
5 
6 public static int FuncB(int x)
7 {
8     return x + 6;
9 }

如果我们不借助任何的自动化函数,我们可以通过这样的写法来进行组合:

Func<int,int> funcC = x => FuncB(FuncA(x));

 

 

但是我们这里无法使用var,因为C#的自动推断类型无法推断出这个类型。这样我们就有了一个新的函数funcC,我们可以试着执行这个函数看看最终的结果。上面我们通过手动的方式完成了组合,下面我们将编写一个自动化的函数来完成这个操作:

1 public static Func<T1, T3> Compose<T1, T2, T3>(Func<T1, T2> func1, Func<T2, T3> func2)
2 {
3     return x => func2(func1(x));
4 }

接着我们利用这个函数来实现上面的功能:

var funcC = Compose<int, int, int>(FuncA, FuncB);

 

 

但是我们发现我们需要提供泛型参数,而不能依赖类型推断。但如果FuncA和FuncB在此之前显式的声明过则不需要提供泛型参数,例如将FuncA和FuncB写成如下的方式:

Func<int, int> FuncA = x => x + 2;
Func<int, int> FuncB = x => x + 6;

 

 

      这样在调用Compose函数就不需要提供泛型参数了,顺便在这里介绍下其他语言下如何实现相同的功能,在F#中通过 FuncB >> FuncA 来实现,而在Haskell中则是用过 FuncA . FuncB来实现,相比C#来说实现起来就非常的简单。通过上面的例子我们也发现了一个问题,就是函数A的返回类型必须和函数B的参数类型一致,并且在这个函数链中只有首个函数可以拥有多个参数,其他的函数只能拥有一个函数。当然函数链的最后一个函数可以是Action,就是说可以没有返回值,下面笔者写一个可以将三个函数进行组合的自动化函数:

1 public static Func<T1, T4> Compose<T1, T2, T3, T4>(Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3)
2 {
3      return x => func3(func2(func1(x)));
4 }

 

当然实际开发中我们并不需要写,可以直接利用FCSLib中提供的函数。

 

高级的部分应用

      学习过《函数式编程之部分应用》的人一定知道,部分应用就是将需要多个参数的函数,拆成一个函数链,每个函数链都只需要一个参数,假如FuncA需要三个参数,则使用部分应用后调用这个函数就需要按照如下的方式来使用FuncA(2)(3)(2),所以下面的内容笔者不会重复的介绍已经介绍过的内容,如果读者没有学习过,可以进入到上面对应的页面中进行学习。

 

我们知道在C#中如果传入部分应用这个自动化函数中的参数是方法,类型推断会无法工作,那么我们就需要输入繁琐的类型参数,比如下面这种情况:

Functional.Curry<Converter<int,int>,Ienumerable<int>,Ienumerable<int>>(Functional.Map<int,int>);

读者会发现类型参数就占据的一半,上面我们也介绍了如何解决这个问题,所以我们可以写个已经显式声明过类型的函数来封装下Map函数:

public static Func<Converter<int, int>, IEnumerable<int>, IEnumerable<int>> MapDelegate<T1, T2>()
{
      return Map<T1, T2>;
}

 

这样我们在调用Curry函数就不需要提供类型参数了:

Functional.Curry(Functional.MapDelegate<int,int>());

 

 

      至此,类型推断的问题我们就解决了。在实际开发中部分应用虽然十分有用,但是在某些情形下却十分的麻烦,比如函数Filter需要两个算法,最后一个参数为数据。在实际使用中我们都会将两个算法赋进去,而在后面的使用中仅仅只会改变对应的数据,但是在采用部分应用后就显得麻烦了,下面是Filter函数的实现:

 1         public static IEnumerable<R> Filter<T,R>(Func<T,R> map,Func<T, bool> compare, IEnumerable<T> datas)
 2         {
 3             foreach (T item in datas)
 4             {
 5                 if (compare(item))
 6                 {
 7                     yield return map(item);
 8                 }
 9             }
10         }

具体的功能就是通过compare函数判断是否符合条件,然后通过map函数返回需要的部分。我们可以通过如下的方式来调用这个函数:

1             foreach (int x in Filter<int, int>(x => x, x => x <= 10, new int[] { 2, 3, 1, 4, 5, 3, 34 }))
2             {
3                 Console.WriteLine(x);
4             }
5             Console.ReadKey();

 

在采用部分应用前,我们先写出这个函数的Delegate版本,这样我们就可以利用类型推断了:

1         public static Func<Func<T, R>, Func<T, bool>, IEnumerable<T>, IEnumerable<R>> FilterDelegate<T, R>()
2         {
3             return Filter<T, R>;
4         }

 

然后我们就可以轻松的使用Currey函数将其部分应用了,这里笔者直接自己实现了一个Currey函数,并没有使用FCSLib中提供的。读者可以参考下:

1         public static Func<T1,Func<T2,Func<T3,R>>> Currey<T1,T2,T3,R>(Func<T1,T2,T3,R> func)
2         {
3             return x => y => z => func(x, y, z);
4         }

最后我们通过实际的使用来看看:

1             var f = Currey(FilterDelegate<int, int>());
2             foreach (int x in f(x => x)(x => x <= 10)(new int[] { 2, 3, 1, 4, 5, 3, 34 }))
3             {
4                 Console.WriteLine(x);
5             }
6             Console.ReadKey();

 

 

即使这样也很繁琐,所以我们需要进行更高级的部分应用,这里我们需要另一个自动化函数来帮助我们实现:

1         public static Func<T3,R> Apply<T1, T2, T3, R>(Func<T1, Func<T2, Func<T3, R>>> func,T1 arg1,T2 arg2)
2         {
3             return x => func(arg1)(arg2)(x);
4         }

这个函数的作用就是将原本的部分应用的函数变成一个接收两个参数并返回一个只接收一个参数的函数,因为算法部分不会变动,但是数据会经常的变动。下面我们通过一个实际的运用来展示:

 1             var f = Apply(Currey(FilterDelegate<int, int>()), x => x, x => x <= 10);
 2 
 3             foreach (int x in f(new int[] { 2, 3, 1, 4, 5, 3, 34 }))
 4             {
 5                 Console.WriteLine(x);
 6             }
 7             foreach (int x in f(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }))
 8             {
 9                 Console.WriteLine(x);
10             }
11             Console.ReadKey();

 

 

通过这样一番折腾后,我们就得到的我们真正需要的函数了,我们在一开始的时候确定算法。然后在后面的使用中我们就可以只传递数据即可。