代码改变世界

C#函数式编程练习

2013-05-30 13:56  jzl  阅读(428)  评论(0)    收藏  举报

C#3.5后,可以使用lambda表达式、拓展方法、yield关键字(这个C#2.0就有)等技术,非常自然的写出函数式风格的代码。最近看了一些资料,这里略写一些

 

映射

代码如下:

public static IEnumerable<TResult> Map<T, TResult>(this IEnumerable<T> items, Func<T, TResult> func)
        {
            if (func == null)
                throw new ArgumentNullException("func should not be null");
            foreach (var item in items)
            {
                yield return func(item);
            }
        }

实际上就是Select方法(多个重载中的最简单的一个),把T类型映射成TResult类型,T和TResult可以相同,比如都为int;也可以不同,比如一个int,一个String,如下

List<int> l = new List<int>(){1,2,3,4,5};
var result1 = l.Map(a => a.ToString());
var result2 = l.Map(a => a*2);

result1是IEnumerable<String>类型,里面是{“1”,”2”,”3”,”4”,”5”},即把int 转为相应的String

result2是IEnumerable<int>类型,里面是{2,4,6,8,10};

Map方法实际就是把一个映射函数,应用到items上的每一个元素,然后返回结果的集合。

 

过滤

代码如下:

 public static IEnumerable<T> Filter<T>(this IEnumerable<T> items,Func<T,bool> filter )
        {
            //检查ArgumentNullException,为了简便,下面都不写了
            foreach (var item in items)
            {
                if (filter(item))
                    yield return item;
            }
        }

实际上就是Where方法,过滤掉集合中不满足条件的元素。这个就不举例~~

 

Fold

不知道该怎么翻译,直接上代码吧

public static T Fold<T> (this IEnumerable<T> items, T init, Func<T,T,T> func)
{
            T result = init;
            foreach (var item in items)
            {
                result = func(result, item);
            }
            return result;
}

给一个初值,然后不断的迭代集合的每一元素,不好解释,大家自己好好体会一下吧。

这里返回值没必要和集合中的元素一样,不过一样的话,理解起来更简单一下。下面看一下两个用例:

List<int> l = new List<int>(){1,2,3,4,5};
var sum = l.Fold(0, (a, b) => a+b);
var factorial = l.Fold(1, (a, b) => a*b);

IEnumerable<String> ls = l.Map(a => a.ToString());
var result = ls.Fold("jzl:", (a, b) => a + b);   //result 的值为 "jzl:12345"

Sum就是集合元素的和,结果是15,如果初值是1,那个结果就是16.

factorial 就是阶乘了。

 

Monad

这个是Haskell语言的一个让人非常费解的知识点大家可以google一下,函数签名是:

L a –> (a -> L b)  -> L b

具体点,把L想象成List,a、b想象成int、String之类的类型,函数接受两个参数,List<a>和一个函数(这个函数接受a类型的参数,然后返回List<b>),然后返回List<b>

//L a -> (a -> L b) -> L b
        //Monad(1)
        public static IEnumerable<B> Monad<A,B>(this IEnumerable<A> items, Func<A,IEnumerable<B>> func  )
        {
            foreach (var item in items)
            {
                foreach (var i in func(item))
                {
                    yield return i;
                }
            }
        }

        //Monad(2)
        public static Nullable<B> Monad<A,B>(this Nullable<A> nA,Func<A,Nullable<B>> func )
            where A:struct
            where B:struct 
        {
            if (!nA.HasValue)
                return null;
            else
                return  func(nA.Value);
        }

对于Monad(1),其实就是SelectMany方法,不过C#编译器对SelectMany方法提供了一些语法糖,写起来会更自然。

对于Monad(2),首先对照着函数签名,好好体会一下。这里虚拟一个场景,来说明它的用法

有个人为了保持平衡(Balance),左手和右手拿东西的差值不能大于3,现在有2个操作,向这个人的左手添加东西和向右手添加东西。然后我不断添加,问最后他能不能保持平衡。代码模拟一下吧:

  //Nullable<T>,T必须是值类型
        public struct Balance
        {
            private int _left;
            private int _right;
            public Balance(int left,int right)
            {
                this._left = left;
                this._right = right;
            }

            public Nullable<Balance> AddLeft(int l)
            {
                if ( Math.Abs(_left + l - _right) > 3 )
                    return null;
                else 
                    return new Balance(_left+l,_right);
            }

            public Nullable<Balance> AddRight(int r)
            {
                if (Math.Abs(_right + r - _left) > 3)
                    return null;
                else
                    return new Balance(_left,_right+r);
            }
        }
//....

 static void Main(string[] args)
        {
            Nullable<Balance> balance = new Balance(0,0);

            var r = balance.Monad(b => b.AddLeft(1))
                .Monad(b => b.AddRight(1));
            var r1 = balance.Monad(b => b.AddLeft(10))
                .Monad(b => b.AddRight(10));
            
            Console.WriteLine(r.HasValue );//True
            Console.WriteLine(r1.HasValue);//False
          
            Console.ReadKey();
        }

中间任何一个时刻,平衡被打破,即左右手之差大于3,那么就会产生Null,然后一直是Null,所以最终结果是Null的话,就知道中间有个过程出错了。如果只需要最终结果,这种调用非常直观有效。

这个例子的问题在于,我最终知道错了,却无法确定哪里错了。这需要在出错的时候,记录错误信息,并传下去。

大家注意理解思想,一个更实际的例子是编译器的Parser,可以一直解析,最终看一下结果,就可以知道中间出错了没有,当然需要传递出错的行号、列号、Error信息等。

编译的例子是听一位大神讲的,暂时还没能通过代码演示出来。不过可以想象一下,Parser实际就是一个状态机,根据输入,不断从当前状态转到下一个状态,如果状态是Error,那么就一直Error下去吧。Monad可以很好的体现这一过程。

挺绕吧,实际本人对Monad也是一知半解,如果有错,请大神们赐教。