先说一个悖论吧。
程序员的任务是让机器帮助人类做工作。
程序员的唯一工作就是开发代码。
程序员喜欢手动hack开发代码。
这就是我今天想要讨论的。
今天读到了阮一峰翻译的为什么Lisp语言如此先进?感触很深。原文Revenge of the Nerds
正如我开头说明的一样,之所以软件工程这个领域这么奇怪,有什么人月神话;有各种其他工程都完全不可能遇见的奇怪问题;即使开发得非常熟悉的类型的项目,再去开发仍然无法准确估计工期和成本;最讨厌的代码就是别人的代码,要知道别的工程领域流水线都是最重要的提高效率的方案,也就是在其他人做了某个步骤的产品基础之上在做一个步骤;即使软件工程理论已经存在和发展了几十年,软件项目的可测性和可控性都完全没有提高;被业界高呼了二十年的面向对象被骂为骗局;等等等等。。。
之所以这一切问题存在,是因为软件工程根本就是一个悖论。当别的工程领域早就摆脱了手工作坊生产,早就工业化自动化了的时候,软件工程还在手动hack代码。就像摆着高大的吊车和推土机不用,软件工程师们想要徒手建造长城,一次又一次的非要从地基开始重新建起。
这里再附上之前在代码交叉拷贝悖论中同ownwaterloo的讨论吧,在读到上述那篇文章之前,其实我们已经走到某一个角落了,其实已经离这个真理只差一步之遥了。(好吧。。虽然我说的也不见得就是啥“真理”啦。。)
OwnWaterloo at 10/11/2010 18:12
不知道是不是想要这样的效果? 伪代码大致是这样:
for image in [ thumb, caption, buttom /* 这里还可以更多 */ ] :
image.offset(height)
image.rotation(270)
hawk at 10/11/2010 23:58
呜!这段代码很漂亮!
可惜c++效仿起来有难度。。
OwnWaterloo at 10/12/2010 10:36
其实那代码是一个暗示……
做GUI这种效率不吃紧的东西, 能不用C++, 就别用C++……
如果我早点学python, 并且发现OpenCV其实有python的绑定……
不知道可以节省多少时间…… 陪gf玩玩什么的……
hawk at 10/12/2010 16:25
那时候因为是在winCE上做widget,没有.NET,所以只能用C++了。。
现在做界面都尽可能用网页或者.NET了。。。
OwnWaterloo at 10/13/2010 12:37
.net也行, 它至少有元数据, 可以”按名字调用”。
C++在运行前完成”名字到地址的转换”, 运行时就没函数名了。
其实按今天的内存和磁盘容量来说, 元数据并不大。
但需要它的时候, 真离不开它……
貌似C++0x也不打算加入……
hawk at 10/14/2010 23:27
是的,javascript那种字符串直接当代码调用的功能强大得一塌糊涂!!
是的,.net的按名字调用,甚至javascript的自解释。为什么他们就那么强大呢?到lisp这里就找到祖宗了。可能就像文章中说的“格林斯潘第十定律”一样,在你追求代码自动化的过程中,你最终就会找到祖宗那里去了。
当然了,我相信lisp肯定也不是最终的完美。但是它是一个指引,指导我们什么是我们想要的。那就是自动化的编程。以前见过貌似是《程序员修炼之道》里面提到过的unix哲学理念,其中有一条是被人大为忽视的,就是让程序自动生成代码。刚在网上翻了一下,维基上的说法是“六:尽可能地榨取软件的全部价值”[引用]。这点是关键吧。软件工程应该像其他的工程一样,尽可能工业化、自动化,早日脱离3、5人小团队的作坊式生产(当然这样说很可能导致一批人失业,不过那是十年或者二十年后的事情了)。当然手工生产会留下来,做那些最精细,最高难,计算机无法自动完成的事情。另外就是确定需求分析设计这部分,这部分是机器无法自动化的。
当然,软件行业变成当今的现状是很自然的。程序员都是聪明而高傲的。他们喜欢证明自己更优秀。他们喜欢hack。他们瞧不起催进度的老板和搞需求的公关。并且程序究竟该怎么开发这点烂事是企业里的管理人员无法操心的。他们看不见那些无形的重型机械被程序员抛开,也看不见程序员其实是在手工修建长城。但是程序员应该自己反省。是的,一次两次骗过肥头胖脑的老板可能蛮有趣的,但是总是手动hack程序员就偏离了自己职业的宗旨-让机器帮助人。
我一直以来都是c\c++的忠实拥趸,藐视高级语言,喜欢重度hack,喜欢重头发明轮子。但是最近的反思,加上这次看到的文章,让我大大醒悟了。高级语言的真正高级之处,就是他强大的自动生成代码的能力啊!
作为一个高傲的程序员,低头使用别人的代码生成器总有“食嗟来之食”的感觉(当然这是不对的。。当真正要解决问题的时候我还是会用合适的工具的!lisp当然也在学,python和perl也在学,也知道用网页和.net来做一些事情了,也体会到.net和js的强大了)。那就自己去发掘,自己去创造。这话说得貌似又变成重新发明轮子了。当然工程实践中不会这么去做的。但是作为理论学习的方向,这方面应该是重点。我研究生专业学的是嵌入式方向,学的是最底层最hack的东西。学的是怎么把c当成汇编来用,怎么把编程看成是操作实际硬件。假如沉迷于此的话,我的软件观就彻底歪掉了。这些东西都算不上是软件。只不过是硬件接口罢了。软件的宗旨是正好相反的。软件的任务就是形而上学,就是纯粹的逻辑和理学的完美。只有结合了天堂的圣洁和地域的邪恶,才能创造出人类世界。
另外一点就是借着AIIDE的契机,学习了机器学习和人工智能的东西。这是软件工程未来的一个新的方向。就是用人工智能来开发代码。是的,不仅仅是用人工智能来解决问题,还要让人工智能来开发程序,让人工智能来开发人工智能。那其实是人工智能提出的时候最早的目标。可惜大多数学习这个领域的人都专注于用它做其他的事情了。遗传编程,一开始就是为了让程序自动创造程序而研究出来的,可惜现在都在做其他的解决方案。
可能说到人工智能产生人工智能,有些人就要想到黑客帝国了。一方面作为理论研究不应该考虑太多伦理问题,伦理是随着理论进步的。另一方面人工智能不等于生命,再高级也不等于生命。关于他的纯哲学讨论我放到下一篇博客去讲吧。估计这里的人多数不会有兴趣看的。
总之今天就像醍醐灌顶了一样一下子有了很多新想法。可惜我的问题在于想法太多动手太少。。。要努力实践!
10 comments on “自动编程 – 语言的级别”
那种强大也是有代价的: 牺牲了效率。
上一篇是因为有gui开发(以及暗含的gui的效率不是热点)这个前提。
缺少这个前提, 两者之间的比较还不好说。
唔。。我知道效率是问题。所以是希望他只是自动生成代码,而不是变成运行时解释。
语言理论的进步还是个循序渐进的过程吧。
所以我说“天堂和地狱结合才是人间嘛”,就是说一方面是追求性能,一方面是追求形式上的完美吧。。
其实C++实现那段代码是可以的, 比较丑。
要优美的实现那段, 只差了一个东西:
auto c = Container(t, c, b);
c.Rotation(270); // 就是这里, 如何依次转发到t,c,b中去?
有一些可选的方案, 但都有不足的地方
1. 侵入式名字
容器的实现类似boost::tuple, 但是有一个Rotation的成员, 转发到各个elem的Rotation。
STL就是这么搭建起来的。
比如stack适配器的底层容器必须有push_back和back, 不能叫add和last。
2. 实现继承
template C : T {};
C a;
a.xxx(); 就会转发到 T::xxx();
但这个对实现Container 好像没什么帮助, 因为它不是简单的转发, 而是依次。
3. 函数指针
Container代码不方便写, 写个for_each表意:
template F for_each(I first, I last, F f) {
for (; first!=last; ++first) f(*first); // call
return f;
}
for_each(c.begin(), c.end(), XXX ); // pass
在pass处传入(一个地址), 在call处调用
应用到Container上, 也会遇见麻烦: T::Rotation, C::Rotation, B::Rotaion同样没有相同的类型。
所以, 最终可能会变成这样:
function f( bind(&T::Rotation, t), bind(&C::Rotation, c), bind(&B::Rotation, b) ); // 语法可能不是这样
f(270);
这就一点也不优美了。
使用纯粹的“面向对象”和消息机制可能可以比较优美的解决这个问题。
就是把类做得像是COM组件。
foreach(IDispatch* p in …){
try{
p->Invoke(“rotation”,…);
}
}
只是伪代码。想用C写得特别漂亮比较困难,不过思路大概是这样了。
前一篇提到: C++运行前必须将名字转换为地址。
但其实不仅仅如此, 在传递参数(运行时函数参数, 或编译时模板非类型参数) 的时候, 也必须将名字转换为地址, 然后传递地址。
也就是说, C++中指代一个地址的, 只有指针, 不能按名字来指代一个地址。
缺的也就是这个东西: 用一个编译时的字符串常量指代一个符号, 推迟”名字到地址的转换”。
如果, for_each可以这么实现:
template symbol for_each(I first, I last, symbol f) {
for (; first!=last; ++first) f(*first); // call
return f;
}
for_each(c.begin(), c.end(), xxx ); // pass
上面那个for_each, 是在pass处将symbol 决议为address。
而下面这个for_each, pass处传递的依然是symbol, 决议在call处。
或者, 更明显一点:
for_each(c.begin(), c.end(), “xxx” );
这在(编译器实现)技术上是完全没有问题的, 只是C++不支持。
若C++支持了, 那Container的问题就很好办了, 根本不需要Container了, 只需要tuple
auto t = tuple(t,c,b);
for_each(t, “Rotation”, 270);
啊,我们想到一块了,你说的这个字符串调用的想法,其实就是IDispatch嘛。。
设想的symbol依然是编译时决议一个地址。
foreach(IDispatch* p in …){
try{
p->Invoke(“rotation”,…); // 如果这行是真实代码, 那肯定不是编译时决议的
}
}
IDispatch::Invoke(char const* name) {
运行时根据name找到一个地址, 调用
}
和lua这些没区别了
呜,明白你的意思了。
IDispatch是微软已经实现的COM接口,支持按名字调用,确实性能是个问题。。
晕…… 有许多 被吃了……
圣洁与邪恶的结合,我喜欢这个比喻。
用AI来再生AI,确实是相当有趣的命题,很期待你那篇文章啊。
Comments are closed.