你,一个DotNet程序员,刚刚加入一个新项目组。除了你之外,其他的成员包括:Ceer,一直从事C项目的程序员,他刚刚转入C#不到一个月; Jally,整天抱着本Design Pattern(没错,就是GoF的那本)在啃的前Java程序员;以及Semon,你对他完全不了解,只是听PM介绍说他是搞Scheme的(传说中的第二古老的语言LISP的方言之一)。不过你也没在意,毕竟计算机这玩意,老东西是不吃香的。
周一,刚打开电脑,老板就跑到你们组的办公座面前:“好吧,伙计们,现在有个function需要你们来搞定。具体是这样的:用户输入2个数,并输入一个操作符。你根据输入的情况来得出相应的运算结果。“
Example: Foo(+, 1, 2) = 3; Foo(*, 3, 6) = 18; Foo(/, 2, 4) = 0.5
Ceer最先作出反应:简单嘛,判断一下输入的操作符就好了。说着,他很快在白板上写出如下代码:
public class CStyle_Calculator
{
static public double Foo(char op, double x, double y)
{
switch(op)
case '+': return x + y; break;
case '-': return x - y; break;
case '*': return x * y; break;
case '/': return x / y; break;
default: throw new Exception(”What the Hell you have input?");
}
}
Jally只看了一遍,就捂着鼻子连连摇头:好一股的代码臭味【注1】。还不如看我用OO的方法来解决:
public interface I操作符 //谁说代码不能写中文的?恩恩
{
double 运算(double x, double y);
}
public class OO_Calculator
{
private I操作符 m_op;
public OO_Calculator(I操作符 op)
{
this.m_op = op; //依赖注入【注2】
}
public double Foo(double x, double y)
{
return this.m_op.运算(x, y);
}
}
public class 加法:I操作符
{
public double 运算(double x, double y)
{
return x + y;
}
}
public class 减法:I操作符
{
public double 运算(double x, double y)
{
return x - y;
}
}
public class 乘法:I操作符
{
public double 运算(double x, double y)
{
return x * y;
}
}
public class 除法:I操作符
{
public double 运算(double x, double y)
{
return x / y;
}
}
public class TheMainClass
{
static public void Main()
{
I操作符 我的加法 = new 加法();
OO_Calculator 我的加法器 = new OO_Calculator(我的加法);
double sum = 我的加法器.Foo(3, 4);
System.Console.WriteLine(sum);
//sum = 7
//其他3个我就不废话了
}
}
你看着Jally把白板写得密密麻麻之后,耸耸肩,暗叹,你们这些用java的废柴,就一个运算器还搞出Interface这些东西,烦不烦啊。 让你们见识见识DotNet的强大吧. 那个运算符我直接用delegate传进去不就好了么.
public delegate double TheOperator(double x, double y);
public class Operators
{
static public double Add(double x, double y)
{
return x + y;
}
static public double Sub(double x, double y)
{
return x - y;
}
//乘,除法 我也懒得废话了
}
public class DotNet_Calculator
{
public double Foo(TheOperator op, double x, double y)
{
return op(x, y);
}
}
public class TheMainClass
{
static public void Main()
{
TheOperator myAdd = new TheOperator(Operators.Add);
TheOperator mySub = new TheOperator(Operators.Sub);
DotNet_Calculator dc = new DotNet_Calculator();
double sum = dc.Foo(myAdd, 2, 4); //sum = 6
System.Console.WriteLine(sum);
double sub = dc.Foo(mySub, 3, 7); //sub = -4
System.Console.WriteLine(sub);
}
}
//dot net 下面还可以用CodeDom动态构造C#代码,然后在内存编译运行。
//如果觉得专门写个Operators很烦的话,可以试试C#2.0的匿名方法
很好,当你写完代码之后,挑衅的看着Jally,Ceer却开始抱怨起来:”这不就是C里面的函数指针么,我也会...“
“然则DotNet下面的Delegate是类型安全滴...”你继续洋洋得意.
而Semon,看了看你们3位华丽的代码,啥也没说,只是在键盘上敲下了2行代码
(define (Foo op x y)
(op x y))
然后就下班了...
【注: scheme的代码稍微解释下:(+ 1 2) = 3, (* 3 4) = 12.】
至于Semon的解法:
(define (Foo op x y)
(op x y))
看明白了么,上面的代码只有一个作用:第一行是函数头,定义了一个叫Foo的函数。该函数接受3个参数op, x, y。
第二行定义了函数的行为:把第一个参数op当作运算符,计算后面2个参数。
所以:(Foo + 1 2) = 3. (Foo / 12 6) = 2.
好了好了,不编故事了。
我只是想简单的让大家在繁忙的工作之余,也瞅瞅Function Programming(函数编程)世界的美妙。函数编程,最大的特点是它是将函数作为语言里1st class的元素来对待的。一个函数可以接受另一个函数作为参数,也可以把一个函数作为结果来返回。这样的函数我们称为Higher-order function。
那么,Function Programming和我们传统的面向对象有啥区别捏? 恩,这个嘛,扯得远可以扯到图灵机和冯·诺以曼这2种体系的差异...@_@不过那个太学术性,俺就不说了。不过有句话可以较好的概括FP和OO的区别(好吧,这个也是抄“紫皮书”上面的):
“Pascal是为了建造金字塔...Lisp是为了建造有机体...”“作为Lisp的内在数据结构,表对于这种可用性起着重要的提升作用...”“采用100函数在一个数据结构上操作,远远优于采用10个操作在十个数据结构上工作”“金字塔矗立在那里千年不变,而有机体则必须演化,否则就会消亡”。
而另一个总结得比较好的话是:(同样是抄来的)
一个对象:一组相同的运算上面,外加不同的数据。(想想你的object,是不是这样的?)
一个Closure:一组相同的数据,外加不同的操作。(Delegate就是这样的思想,有兴趣的话也可以去看看Ruby)
基本上,恩,没啥说的了。 如果你感兴趣的话,可以去看MIT SICP的课程(有在线版的,MIT也作为Open Course开设了的)
参考文献:
Java 语言中的函数编程(偶FP的入门贴。查叔叔,我膜拜您)
http://www.hibernate.org.cn/viewtopic.php?t=7569&postdays=0&postorder=asc&start=0
Lambda Calculus
http://www.mactech.com/articles/mactech/Vol.07/07.05/LambdaCalculus/
Java 语言中的函数编程
http://www-128.ibm.com/developerworks/cn/java/j-fp/
【注1】
见Bob大叔的《ASD》一书
【注2】
Flower的依赖注入模式,Ioc容器啥的是这里来的
2008年12月9日星期二
伟大的树形结构
一篇介绍Ant、Xml与Lisp相似,来讲述Lisp语言的特质,数据与指令;另外,那篇文章也谈到以xml可以作为语言间相互转换的中间语言,当时非常震惊于lisp语言的神奇,称其为与C语言并立的语言高峰。后来随着不断地回味这其中的门道,个人觉得这之间其实不用讲那么玄乎,其实最根本的地方就是树形特征在它们里面暗藏着,使得他们可以存在某种形式的映射关系。即不管是lisp还是c、java、xml其实是一种树形化表示方式,在这种表示方式下,我们采用了不同的解释就可以让它转化为不同的东西。而两种树形化语言间,由于存在某种程度上的映射关系,那么可以表示树形化的xml就,可以作为中间一级的介质存在。另外,由于树形数据在计算机编程中广泛性,我们有理由相信lisp类似列表的树形表示,它的生命力很顽强的。因为只要c语言能够表示的,lisp就可以通过一定的树形化映射表示过去。
谈到对树形化结构的佩服,就谈一点在个人具体工作中,遇到的一个关于树形化的应用。也是现在比较火热的基于Web的B/S系统。在几年的工作经历中,由于特殊的软件产品,曾接触到ActiveX控件对Javascript暴露的方法接口和事件接口,以达到“胖”客户端的目的。在ActiveX的设计中有良有莠,其中有一些接口和事件采用了参数列表的做法,有些接口和事件采用了对象化的做法。在维护的过程发现,由于业务的新变化,经常可能需要扩展新的数据,如果接口使用参数列表的做法,每次为了保持兼容性,我们就不得不开辟新的接口;但是,对象化的接口每次加入新的东西,几乎是对Javascript是透明的,原来的javascript可以照原来的协议进行访问、访问老属性,新功能版本的Javascript工作在新的协议上、即访问新的属性。
在经过几番教训和磨砺后,大家讨论觉得对象化ActiveX控件对脚本暴露的方法和事件是一个获得比较好扩展性、兼容性的做法。在以后的工作过程中,经常以这样的观点去靠、去分析已经实现ActiveX代码,发现在所有对象化接口和事件中,另外一个做的非常好的就是对象化其实是一个递归的过程,即对象化的属性也可以是对象化的.这样对象化自身看来就像一个背袋,可以不断地扩展,往背袋里面装载了东西,它可以是原来版本已经有,也可以是后来加入的。背袋提供了类似容器一样的扩展性,对于背袋的外层用户来说,如果是旧版本它可以按照老的工作协议从背袋中取数据,如果是新版本则可以按照新的工作协议。
在这个对象化的最后,你也可以看到一颗树的产生,它由自动化接口IDispatch作为树的主干存在,原始类型以及自动化接口自身可以作为叶子存在,形式化可描述为IDispatch* = {IDispatch*|other variant Type}。后来也思考到由于Javascript之于对象化的Activex接口和事件有天生的兼容性和扩展性,那么强类型语言之于二进制的数据呢?我们是不是也可以做到,修改接口协议之后获得自动的兼容性呢?我们知道是数据结构决定了操作,或者操作上的某种特性的要求也会支持去创造出新的数据结构。对于二进制数据,如果在操作上是普通的移位,而不是象良性树形化数据,我们可以按照节点去操作,那么二进制的扩展性就不是很好。因为插入新数据后,简单地移位后会导致后面的发生变化。我们在二进制协议设计中也通常使用TLV代表一个存储域,其实它就可以看做是一种类似树形的节点,那么我们看看在TLV结构下的节点操作吧,我们可以做到每次我二进制的移动位置量是由TLV中的L决定,这样移动量是一个节点,相当于节点操作,而Value的取法仅决定于工作协议,没有严格与TLV中Length相绑定滚系。 在这种二进制TLV的组织中和“正确”编解码中,那么我们也可以获得类似树形化数据的扩展性,随便添加枝叶,但是对已有的接口程序不会存在不良影响。
谈到对树形化结构的佩服,就谈一点在个人具体工作中,遇到的一个关于树形化的应用。也是现在比较火热的基于Web的B/S系统。在几年的工作经历中,由于特殊的软件产品,曾接触到ActiveX控件对Javascript暴露的方法接口和事件接口,以达到“胖”客户端的目的。在ActiveX的设计中有良有莠,其中有一些接口和事件采用了参数列表的做法,有些接口和事件采用了对象化的做法。在维护的过程发现,由于业务的新变化,经常可能需要扩展新的数据,如果接口使用参数列表的做法,每次为了保持兼容性,我们就不得不开辟新的接口;但是,对象化的接口每次加入新的东西,几乎是对Javascript是透明的,原来的javascript可以照原来的协议进行访问、访问老属性,新功能版本的Javascript工作在新的协议上、即访问新的属性。
在经过几番教训和磨砺后,大家讨论觉得对象化ActiveX控件对脚本暴露的方法和事件是一个获得比较好扩展性、兼容性的做法。在以后的工作过程中,经常以这样的观点去靠、去分析已经实现ActiveX代码,发现在所有对象化接口和事件中,另外一个做的非常好的就是对象化其实是一个递归的过程,即对象化的属性也可以是对象化的.这样对象化自身看来就像一个背袋,可以不断地扩展,往背袋里面装载了东西,它可以是原来版本已经有,也可以是后来加入的。背袋提供了类似容器一样的扩展性,对于背袋的外层用户来说,如果是旧版本它可以按照老的工作协议从背袋中取数据,如果是新版本则可以按照新的工作协议。
在这个对象化的最后,你也可以看到一颗树的产生,它由自动化接口IDispatch作为树的主干存在,原始类型以及自动化接口自身可以作为叶子存在,形式化可描述为IDispatch* = {IDispatch*|other variant Type}。后来也思考到由于Javascript之于对象化的Activex接口和事件有天生的兼容性和扩展性,那么强类型语言之于二进制的数据呢?我们是不是也可以做到,修改接口协议之后获得自动的兼容性呢?我们知道是数据结构决定了操作,或者操作上的某种特性的要求也会支持去创造出新的数据结构。对于二进制数据,如果在操作上是普通的移位,而不是象良性树形化数据,我们可以按照节点去操作,那么二进制的扩展性就不是很好。因为插入新数据后,简单地移位后会导致后面的发生变化。我们在二进制协议设计中也通常使用TLV代表一个存储域,其实它就可以看做是一种类似树形的节点,那么我们看看在TLV结构下的节点操作吧,我们可以做到每次我二进制的移动位置量是由TLV中的L决定,这样移动量是一个节点,相当于节点操作,而Value的取法仅决定于工作协议,没有严格与TLV中Length相绑定滚系。 在这种二进制TLV的组织中和“正确”编解码中,那么我们也可以获得类似树形化数据的扩展性,随便添加枝叶,但是对已有的接口程序不会存在不良影响。
订阅:
博文 (Atom)