编程语言的发展趋势:声明式动态并发

概述:国内的计算机教育和工程培训,彷佛一直在宣传“语言不重要,重要的是思想”,“语言一通百通”等观点,甚至在许多人眼中“语言的讨论”彻底是不入流的,但其实“编程语言”与“工具”、“框架”或是“开发方法”等事物同样,都对生产力有着重要的影响。事实上,语言的发展历史比其余方面更为悠久,而且在过去十几年,甚至最近几年中都依然在不断的碰撞,演变。

  程序设计离不开编程语言,可是编程语言在国内的大环境中彷佛一直是个二等公民。国内的计算机教育和工程培训,彷佛一直在宣传“语言不重要,重要的是思想”,“语言一通百通”等观点,甚至在许多人眼中“语言的讨论”彻底是不入流的,但其实“编程语言”与“工具”、“框架”或是“开发方法”等事物同样,都对生产力有着重要的影响。事实上,语言的发展历史比其余方面更为悠久,而且在过去十几年,甚至最近几年中都依然在不断的碰撞,演变。期间一些新的语言诞生了,而另外一些在当时看来阳春白雪的语言和编程范式也从新得到了人们的重视。程序员

  Anders Hejlsberg是微软的Technical Fellow,担任C#编程语言的首席架构师,也参与了.NET Framework,以及VB.NET和F#等语言的设计与开发。几个月前,Anders在比利时的TechDays 2010及荷兰DevDays 2010分别进行了一场演讲,阐述了他眼中对于编程语言的发展趋势及将来方向,本文便对他的观点进行了总结。正则表达式

  大约25到30年前,Anders开发了著名的Turbo Pascal,这是一套集语言、编译器及开发工具于一体的产品,这也是Anders进入编程语言这一领域的起点。Anders谈到,现在的计算机和当年他开发的Turbo Pascal所用的Z-80已经不可同日而语。从那时算起,现在的机器已经有大约10万倍的外部存储容量,1万倍的内存大小,CPU速度也有大约1000倍的提升。可是,若是咱们比较现在的Java代码及当年Pascal代码,会发现它们的差异其实并不大。Anders认为编程语言的发展很是缓慢,期间固然出现了一些东西,例如面向对象等等,可是远没有好上1000倍。事实上,近几十年来的努力主要体如今框架及工具等方面(以下图)。例如.NET Framework里有超过一万个类及十万个方法,与Turbo Pascal相比的确有了超过1000倍的增加。一样相似,如今的IDE包含了无数强大的功能,例如语法提示,重构,调试器等等。与此相比,编程语言的改进的确很不明显。数据库

  在过去五、60年的编程历史中,编程语言的抽象级别不断提升,人们都在努力让编程语言更有表现力,这样咱们能够用更少的代码完成更多的工做。咱们一开始使用汇编,而后使用面向过程的语言(如Pascal和C),而后是面向对象语言(如C++),随后便进入了托管时代,语言运行于受托管的执行环境上(如C#,Java),它们的主要特性有自动的垃圾收集,类型安全等等。Anders认为这样的趋势还会继续保持下去,咱们还会看到抽象级别愈来愈高的语言,而语言的设计者则必须理解并预测下一个抽象级别是什么样子的。另外一方面,如.NET,Java等框架的重要性提升了许多,编程语言每每都倾向于构建于现有的工具上,而不会从头写起。如今出现的编程语言,例如F#,以及Java领域的Scala,Clojure等等,它们都是基于现有框架构建的,每次从头开始的代价实在过高。编程

  在Anders眼中,现在影响力较大的趋势主要有三种(以下图),它们分别是“声明式的编程风格”(包括“领域特定语言”及“函数式编程”)、过去的五年很是火热的“动态语言”(其最重要的方面即是“元编程”能力)以及多核环境下的“并发编程。此外随着语言的发展,本来经常使用的“面向对象”语言,“动态语言”或是“函数式”等边界也变得愈来愈模糊,例如各类主要的编程语言都受到函数式语言的影响。所以,“多范式”程序设计语言也是一个愈发明显的趋势。浏览器

  声明式编程与DSL安全

  目前常见的编程语言大都是命令式(Imperative)的,例如C#,Java或是C++等等。这些语言的特征在于,代码里不只表现了“作什么(What)”,而更多表现出“如何(How)完成工做”这样的实现细节,例如for循环,i += 1等等,甚至这部分细节会掩盖了咱们的“最终目标”。在Anders看来,命令式编程一般会让代码变得十分冗余,更重要的是因为它提供了过于具体的指令,这样执行代码的基础设施(如CLR或JVM)没有太多发挥空间,只能老老实实地根据指令一步步的向目标前进。例如,并行执行程序会变得十分困难,由于像“执行目的”这样更高层次的信息已经丢失了。所以,编程语言的趋势之一,即是能让代码包含更多的“What”,而不是“How”,这样执行环境即可以更加聪明地去适应当前的执行要求。性能优化

  关于声明式的编程风格,Anders主要提出了两个方面,第一个方面是DSL(Domain Specific Language,领域特定语言)。DSL不是什么新鲜的玩意儿,咱们平时常常接触的SQL,CSS,正则表达式等等都属于DSL。有的DSL可能更加专一于一个方面,例如Mathematica,LOGO等等。这些语言的目标都是特定的领域,与之相对的则是GPPL(General Purpose Programming Language,通用目的编程语言)。Martin Fowler将DSL分为外部DSL及内部DSL两种。外部DSL有本身的特定语法、解析器和词法分析器等等,它们每每是一种小型的编程语言,甚至不会像GPPL那样须要源文件。与之相对的则是内部DSL。内部DSL其实更像是种别称,它表明一类特别API及使用模式。数据结构

  XSLT,SQL等等均可以算做是外部DSL。外部DSL通常会直接针对特定的领域设计,而不考虑其余方面。James Gosling曾经说过:每一个配置文件最终都会变成一门编程语言。一开始您可能只会用它表示一点点东西,慢慢地您便会想要一些规则,而这些规则则变成了表达式,后来您可能还会定义变量,进行条件判断等等,而最终它就变成了一种奇怪的编程语言,这样的状况家常便饭。如今有一些公司也在关注DSL的开发。例如之前在微软工做的Charles Simonyi提出了Intentional Programming的概念,还有JetBrains公司提供的一个叫作MPS(Meta Programming System)的产品。最近微软也提出了本身的Oslo项目,而在Eclipse世界里也有Xtext,因此其实现在在这方面也有很多人在尝试。因为外部DSL的独立性,在某些状况下也会出现特定的工具,辅助领域专家或是开发人员自己编写DSL代码。还有一些DSL会以XML方言的形式提出,利用XML方言的好处在于有很多现成的工具可用,这样能够更快地定义本身的语法。架构

  而内部DSL,正像以前提到的那样,它每每只是表明了一系列特别的API及使用模式,例如LINQ查询语句及Ruby on Rails中的Active Record声明代码等等。内部DSL可使用一系列API来“假装”成一种DSL,它每每会利用一些“流畅化”的技巧,例如像jQuery那样把一些方法经过“点”链接起来,而另外一些也会利用元编程的方式。内部DSL还有一些优点,例如能够访问语言中的代码或变量,以及利用代码补全,重构等母语言的全部特性。 并发

  DSL的可读性每每很高。例如,要筛选出单价大于20的产品,并对所属种类进行分组,并降序地列出每组的分类名称及产品数量。若是是用命令式的编程方式,则多是这样的:

#div_code img{border:0px;}
Dictionary<string, Grouping> groups = new Dictionary<string, Grouping>();
foreach (Product p in products)
{
if (p.UnitPrice >= 20)
{
if (!groups.ContainsKey(p.CategoryName))
{
Grouping r = new Grouping();
r.CategoryName = p.CategoryName;
r.ProductCount = 0;
groups[p.CategoryName] = r;
}
groups[p.CategoryName].ProductCount++;
}
}

List<Grouping> result = new List<Grouping>(groups.Values);
result.Sort(delegate(Grouping x, Grouping y)
{
return
x.ProductCount > y.ProductCount ? -1 :
x.ProductCount < y.ProductCount ? 1 :
0;
});
 

  显然这些代码编写起来须要一点时间,且很难直接看出它的真实目的,换言之“What”几乎彻底被“How”所代替了。这样,一个新的程序员必须花费必定时间才能理解这段代码的目的。但若是使用LINQ,代码即可以改写成:

#div_code img{border:0px;}
var result = products
.Where(p => p.UnitPrice >= 20)
.GroupBy(p => p.CategoryName)
.OrderByDescending(g => g.Count())
.Select(g => new { CategoryName = g.Key, ProductCount = g.Count() });
 

  这段代码更加关注的是“What”而不是“How”,它不会明确地给出过滤的“操做方式”,也没有涉及到建立字典这样的细节。这段代码还能够利用C# 3.0中内置的DSL,即LINQ查询语句来改写:

#div_code img{border:0px;}
var result =
from p in products
where p.UnitPrice >= 20
group p by p.CategoryName into g
orderby g.Count() descending
select new { CategoryName = g.Key, ProductCount = g.Count() };
 

  编译器会简单地将LINQ差距语句转化为前一种形式。这段代码只是表现出最终的目的,而不是明确指定作事的方式,这样即可以很容易地并行执行这段代码,如使用PINQ则几乎不须要作出任何修改。

  函数式编程

  Anders提出的另外一个重要的声明式编程方式即是函数式编程。函数式编程历史悠久,它几乎和编程语言自己同时诞生,如当年的LISP即是个函数式编程语言。除了LISP之外还有其余许多函数式编程语言,如APL、Haskell、ML等等。关于函数式编程在学术界已经有过许多研究了,大约在5到10年前许多人开始吸取和整理这些研究内容,想要把它们融入更为通用的编程语言。如今的编程语言,如C#、Python、Ruby、Scala等等,它们都受到了函数式编程语言的影响。

  使用命令式编程语言写程序时,咱们常常会编写如x = x + 1这样的语句,此时咱们大量依赖的是可变状态,或者说是“变量”,它们的值能够随程序运行而改变。可变状态很是强大,但随之而来的即是被称为“反作用”的问题,例如一个无需参数的void方法,它会根据调用次数或是在哪一个线程上进行调用对程序产生影响,它会改变程序内部的状态,从而影响以后的运行效果。而在函数式编程中则不会出现这个状况,由于全部的状态都是不可变的。事实上对函数式编程的讨论更像是数学、公式,而不是程序语句,如x = x + 1对于数学家来讲,彷佛只是个永不为真的表达式而已。

  函数式编程十分容易并行,由于它在运行时不会修改任何状态,所以不管多少线程在运行时均可以观察到正确的结果。假如两个函数彻底无关,那么它们是并行仍是顺序地执行便没有什么区别了。固然,现实中的程序必定是有反作用的,例如向屏幕输出内容,向Socket传输数据等等,所以真实世界中的函数式编程每每都会考虑如何将有反作用的代码分离出来。函数式编程默认是不可变的,开发人员必须作些额外的事情才能使用可变状态或是危险的反作用,与之相反,如C#或Java必须使用readonly或是final来作到这一点。此时,使用函数式编程语言时的思惟观念便会有所不一样了。

  F#是微软随VS 2010推出的一门函数式编程语言,它基于OCaml的核心部分,所以是一门强类型编程语言,并支持一些如模式匹配,类型推断等现代函数式编程语言的特性。在此之上,F#又增长了异步工做流,度量单位等较为前沿的语言功能。在F#中若是要计算一个列表全部元素之和,也可使用命令式的风格来编写代码:

#div_code img{border:0px;}
let sumSquaresI l = 
let mutable acc = 0
for x in l do
acc <- acc + sqr x
acc
 

  只不过,F#中的一切默认都是不可变的,开发人员须要使用mutable关键字来声明一个可变的状态。事实上,在F#中更典型作法是:

#div_code img{border:0px;}
let rec sumSquaresF l = 
match l with
| [] -> 0
| head :: tail -> sqr head + sumSquaresF tail
 

  在数学里咱们常用递归,把一个公式分解成几个变化的形式,以此进行递归的定义。纯函数式的代码其“数学性”较强,若是您分析上面这段代码,会发现它几乎就是标准的数学定义。在编程时咱们也使用递归的作法,编译器会设法帮咱们转化成尾调用或是循环语句。 

  动态语言与元编程

  动态语言不会严格区分“编译时”和“运行时”。对于一些静态编程语言(如C#),每每是先进行编译,此时可能会获得一些编译期错误,而对于动态语言来讲这两个阶段便混合在一块儿了。常见的动态语言有JavaScript,Python,Ruby,LISP等等。动态语言和静态语言各有一些优点,这也是两个阵营争论多年的内容。不过Anders认为它们各自都有十分重要的优势,而将来不属于其中任何一方。他表示,从编程语言发展过程当中能够观察到两种特色正在合并的趋势,将来应该属于二者的杂交产物。

  许多人认定动态语言执行起来很慢,也没有类型安全等等。例若有这样一段代码:

#div_code img{border:0px;}
var a = 0, n = 10;
for (var i = 0; i < n; i++) {
a += i;
}
 

  这段代码在C#和JavaScript中都是合法的,可是它们的处理方式截然不同。在C#中,编译器能够推断出a和n都是32位整数,则for循环和相加操做都只是简单的CPU指令,天然效率很高。可是对于JavaScript等动态类型语言来讲,var只表明了“一个值”,它能够是任意类型,所以这里其实还会包含一个“类型标记”,代表它在运行时是什么类型的对象。因此二者的区别之一即是,表示一样的值在动态语言中会有一些额外的开销,在现在的CPU中,“空间”也意味着“速度”,因此较大的值便须要较长时间进行处理,这里便损失了一部分效率。此外JavaScript在计算a加i时,那么必须先查看两个变量中的类型标记,根据类型选择出合适的相加操做,而后加载两个值,最后再进行加法操做,一旦越界了还要利用double。很明显在这里也会带来许多开销。通常来讲,动态语言是使用解释器来执行的,所以还有一些解释器须要的二进制码,把这些性能损失所有加起来之后,便会发现执行代码时须要10倍到100倍的性能开销。

  不过近几年出现的一些动态虚拟机或引擎将此类状况改善了许多。现在大部分的JavaScript引擎使用了JIT编译器,因而便省下了解释器的开销,这样性能损失便会减少至3到10倍。而在过去的两三年间,JIT编译器也变得愈来愈高效,浏览器中新一代的适应性JIT编译器,如TraceMonkey,V8,还有微软在IE 9中使用的Chakra引擎。这种适应性的JIT编译器使用了一部分有趣的技术,如Inline Caching、Type Specialization、Hidden Classes、Tracing等等,它们能够将开销下降至2到3倍的范围内,这种效率的提高可谓十分神奇。在Anders看来,JavaScript引擎可能已经接近了性能优化的极限,咱们在效率上能够提高的空间已经很少。不过他一样认为,现在JavaScript语言的性能已经足够快了,彻底有能力做为Web客户端的统治性语言。

  动态语言的关键之一即是“元编程”,“元编程”其实是“代码生成”的一种别称,在平常应用中开发人员其实常常依赖这种作法了。在某些场景下使用动态语言会比静态语言更加天然一些。例如在C#或Java里使用ORM时,一种传统作法是让代码生成器去观察数据库,并生成一大堆代码,而后再编译。而动态语言并无编译期和执行期的区别,例如在Ruby on Rails中使用ActiveRecord便无须定义各式字段。

  Anders谈到,他和他的团队也在努力改进静态语言的元编程能力,如他们正在实现的“编译器即服务(Compiler as a Service)”。传统的编译器是一个黑盒,一端输入代码,而另外一端便会生成.NET程序集等数据,开发人员很难参与或理解它的工做。可是在不少时候,开发人员并不必定须要编译器来生成程序集,他们须要的是一些树状的表现形式,而后对它进行识别和重写。所以,开发人员可能会愈来愈须要一些开放编译器功能的API。这么作可让静态类型语言得到许多有用的功能,包括元编程以及可操做的完整对象模型等等。

  并发

  Anders看来,多核革命的一个有趣之处在于,它会要求并发的思惟方式有所改变。传统的并发思惟,是在单个CPU上执行多个逻辑任务,使用旧有的分时方式或是时间片模型来执行多个任务。可是现在的并发场景则正好相反,是要将一个逻辑上的任务放在多个CPU上执行。这改变了咱们编写程序的方式,这意味着对于语言或是API来讲,咱们须要有办法来分解任务,把它拆分红多个小任务后独立的执行,而传统的编程语言中并不关注这点。

  使用目前的并发API来完成工做并不容易,好比Thread,ThreadPool,Monitor等等,开发人员很难走的太远。不过在.NET 4.0中提供了一套强大的框架,即.NET并行扩展(Parallel Extensions),这是一种现代的并发模型,将逻辑上的任务并发与实际使用的的物理模型分离开来。之前的API都是直接处理线程等基础元素,不过利用.NET并行扩展中的任务并行库(Task Parallel Library),并行LINQ(Parallel LINQ)以及协调数据结构(Coordination Data Structures)让开发人员能够直接关注逻辑上的任务,而没必要关心它们是如何运行的,或是使用了多少个线程和CPU等等。利用LINQ这样的DSL也有助于写出并行的代码,若是使用普通的for循环配合线程池来实现并行,则开发人员很容易在各类API里失去方向。

  不过事实上,编写并行的代码依然很困难,尤为是要识别出能够并行的地方。Anders认为不少时候仍是须要编程语言来关注这方面的事情。好比“隔离性(Isolation)”,即编译器如何发现这段代码是独立的,即可以将其安全地并发执行。某段代码建立了一个对象,在分享给其余人以前,咱们对它的改变是安全的,可是一旦将其共享出去之后便彻底不一样了。所以理想中的类型系统应该能够跟踪到这样的共享,如Linear Types——这在学术界也有一些研究。编程语言也能够在函数的纯洁性(Purity)方面下功夫,如关注某个函数是否有反作用,有些时候编译器能够作这方面的检查,它能够禁止某些操做,以此保证咱们写出无反作用的纯函数。另外即是不可变性(Immutability),目前的语言,如C#或VB,咱们须要额外的工做才能写出不可变的代码。Anders认为合适的作法应该是在语言层面上更好的支持不可变性。这些都是在并发方面须要考虑的问题。
 

  Anders还提到了他在思考并发语言特性时所遵循的原则:一个语言特性不该该针对某个特定的并发模型,而应该是一种通用的,可用于各类不一样的并发场景的特性,就像隔离性、纯洁性及不可变性那样。语言拥有这样的特性以后,就能够用于构建各类不一样的API,各类并发方式均可以利用到核心的语言特性。

  总结

  Anders认为,对于编程语言来讲,如今出现了许多有趣的东西,也是个使人激动的时刻。在过去,大约是1995到2005年,的确能够说是一个编程语言的黄金时期。当Java出现的时候,编程语言的门槛变得平坦了,一切都是Java,彷佛其余编程语言都完蛋了,程序设计者也没什么可作的。不过你们又逐渐发现,其实这远没有结束。如今回顾起来,会发现这段时间又出现了许多有趣的编程语言,这其实也表明了咱们在编程领域上的进步。

相关文章
相关标签/搜索