编程语言的发展趋势by Anders Hejlsberg

这是Anders Hejlsberg比利时TechDays 2010所作的开场演讲javascript

  编程语言的发展很是缓慢,期间也固然出现了一些东西,例如面向对象等等,你可能会想,那么我么这么多年的努力都到哪里去了呢?事实上这些努力没有体如今编程语言上,而是出如今框架及工具等方面了。若是你关注现在咱们使用的框架,它们的体积的确有很大的增加。例如当年Turbo Pascal所带的框架大约有,好比说100个功能,而如今的.NET Framework里则有一万个类,十万个方法,的确有1000倍的增加。与此相似,若是你观察如今的IDE,咱们如今已经有了无数强大的功能,例如语法提示,重构,调试器,探测器等等,这方面的新东西有不少。与此相比,编程语言的改进的确很不明显。java

另外一方面,如.NET,Java等框架的重要性提升了许多。而编程语言每每都倾向于构建于现有的工具上,而不会从头写起。如今出现的编程语言,例如F#,若是你关注Java领域那么还有ScalaClojure等等,它们都是基于现有框架构建的。如今已经有太多东西能够直接利用了,每次从头开始的代价实在过高。jquery

还有件事,即是在过去五、60年的编程历史中,咱们都不断地提升抽象级别,咱们都在不断地让编程语言更有表现力,让咱们能够用更少的代码完成更多的工做。咱们一开始先使用汇编,而后使用面向过程的语言,例如Pascal和C,而后即是面向对象语言,如C++,随后就进入了托管时代──受托管的执行环境,例如.NET,Java,它们的主要特性有自动的垃圾收集,类型安全等等。我目前尚未看出这样的趋势有中止的迹象,所以咱们还会看到抽象级别愈来愈高的语言,而语言的设计者则必须理解并预测下一个抽象级别是什么样子的。程序员

 

咱们会愈来愈多地使用声明式的编程风格。这里我主要会提到例如DSL(Domain Specific Language,领域特定语言)以及函数式编程。而后在过去的五年里,我发现对于动态语言的研究变得很是火热,其中对咱们产生重大影响的无疑是动态语言所拥有的良好的元编程能力,还有一些很是有趣的东西,例如JavaScript引擎的发展。而后即是并发编程,不管咱们愿不肯意,多核的产生都在迫使咱们不得不重视并发编程。web

有一点值得一提,那即是随着语言的发展,本来的编程语言分类方式也要有所改变了。之前咱们常常说面向对象语言,动态语言或是函数式语言。可是咱们如今发现,这些边界变得愈来愈模糊常常会互相学习各自的范式。静态语言中出现了动态类型,动态语言里也出现了静态能力,而现在全部主要的编程语言都受到函数式语言的影响。所以,一个愈来愈明显的趋势是“多范式程序设计语言”。正则表达式

目前咱们在编写软件时大量使用的是命令式(Imperative)编程语言,例如C#,Java或是C++等等。这些语言的特征在于,写出的代码除了表现出“什么(What)”是你想作的事情以外,更多的代码则表现出实现的细节,也就是“如何(How)”完成工做。这部分代码有时候多到掩盖了咱们原来问题的解决方案。好比,你会在代码里写for循环,if语句,a等于b,i加一等等,这体现出机器是如何处理数据。首先,这种作法让代码变得冗余,并且它也很难让执行代码的基础设施更聪明地判断该如何去执行代码。当你写出这样的命令是代码,而后把编译后的中间语言交给虚拟机去执行,此时虚拟机并无多少空间能够影响代码的执行方式,它只能根据指令一条一条老老实实地去执行。例如,咱们如今想要并行地执行程序就很困难了,由于更高层次的一些信息已经丢失了。这样,咱们只能在代码里给出“How”,而不能体现出“What”的信息。数据库

有多种方式能够将“What”转化为更为“声明式”的编程风格,咱们只要可以在代码中体现出更多“What”,而不是“How”的信息,这样执行环境即可以更加聪明地去适应当前的执行要求。例如,它能够决定投入多少CPU进行计算,你的当前硬件是什么样的,等等。编程

如今有两种比较重要的成果,一是DSL(Domain Specific Language,领域特定语言),另外一个则是函数式编程浏览器

其实DSL不是什么新鲜的玩意儿,咱们平时一直在用相似的东西,好比,SQL,CSS,正则表达式,有的可能更加专一于一个方面,例如MathematicaLOGO等等。这些语言的目标都是特定的领域,与之相对的则是GPPL(General Purpose Programming Language,通用目的编程语言)。缓存

Martin Fowler提出DSL应该分为外部DSL及内部DSL两种,我认为这种划分方式仍是比较有意义的。外部DSL是自我包含的语言,它们有本身特定语法、解析器和词法分析器等,它每每是一种小型的编程语言,甚至不会像GPPL那样须要源文件。与之相对的则是内部DSL。内部DSL其实更像是种别称,它表明一类特别API及使用模式。

这些是咱们平时会遇到的一些外部DSL,如这张幻灯片上表现的XSLT,SQL或是Unix脚本。外部DSL的特色是,你在构建这种DSL时,其实扮演的是编程语言设计者的角色,这个工做并不会交给普通人去作。外部DSL通常会直接针对特定的领域设计,而不考虑其余东西。James Gosling曾经说过这样的话,每一个配置文件最终都会变成一门编程语言。你一开始可能只会用它表示一点点东西,而后慢慢你便会想要一些规则,而这些规则则变成了表达式,可能你还会定义变量,进行条件判断等等。而最终它就变成了一种奇怪的编程语言,这样的状况家常便饭。

事实上,如今有一些公司也在关注DSL的开发。例如之前在微软工做的Charles Simonyi提出了Intentional Programming的概念,还有一个叫作JetBrains的公司提供一个叫作MPS(Meta Programming System)的产品。最近微软也提出了本身的Oslo项目,而在Eclipse世界里也有个叫作Xtext的东西,因此其实在这方面如今也有很多人在尝试。

我在观察外部DSL时,每每会关注它的语法到底提供了多少空间,例如一种XML的方言,利用XML方言的好处在于有很多现成的工具可用,这样能够更快地定义本身的语法。

而内部DSL,正像我以前说的那样,它其实只是一系列特别的API及使用模式的别称。这里则是一些LINQ查询语句,Ruby on Rails以及jQuery代码。内部DSL的特色是,它其实只是一系列API,可是你能够“伪装”它们一种DSL。内部DSL每每会利用一些“流畅化”的技巧,例如像这里的LINQ或jQuery那样把一些方法经过“点”链接起来。有些则利用了元编程的方式,如这里的Ruby on Rails就涉及到了一些元编程。这种DSL能够访问语言中的代码或变量,以及利用如代码补全,重构等母语言的全部特性。

如今我会花几分钟时间演示一下我所建立的DSL,也就是LINQ。我相信大家也已经用过很多LINQ了,不过这里我仍是快速的展现一下我所表达的更为“声明式”的编程方式。

public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public string CategoryName { get; set; }
    public int UnitPrice { get; set; }

    public static List<Product> GetProducts() { /* ... */ }
}

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        List<Product> products = Product.GetProducts();

        List<Product> result = new List<Product>();
        foreach (Product p in products)
        {
            if (p.UnitPrice > 20) result.Add(p);
        }

        GridView1.DataSource = result;
        GridView1.DataBind();
    }
}

这里有许多Product对象,那么如今我要筛选出全部单价大于20的那些, 再把他们显示在一个GridView中。传统的作法就是这样,我先获得全部的Product对象,而后foreach遍历每一个对象,再判断每一个对象的单价,最终把数据绑定到GridView里。运行这个程序……(打开页面)这就是就能获得结果。

好,那么如今我要作一些稍微复杂的事情。可能我不是要展现单价超过20的Product对象,而是要查看每一个分类中究竟有多少个单价超过20的对象,而后根据数量进行排序。若是不用DSL完成这个工做,那么我可能会先定义一个对象来表示结果:

class Grouping
{
    public string CategoryName { get; set; }
    public int ProductCount { get; set; }
}

这是个表示分组的对象,用于保存分类的名称和产品数量。而后咱们就会写一些十分丑陋的代码:

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;
});

我先建立一个新的字典,用于保存分类名称到分组的对应关系。而后我遍历每一个Product对象,对于每一个单价大于20的对象,若是字典中尚未保存对应的分组则建立一个,而后将数量加一。而后为了排序,我调用Sort方法,因而我要提供一个委托做为排序方法,而后blablablabla……执行以后……(打开页面)我天然能够获得想要的结果。

可是,首先这些代码写起来须要花费一些时间,很显然。而后仔细观察,你会发现这写代码几乎都是在表示“How”,而“What”基本已经丢失了。假设我离开了,如今新来了一个程序员要维护这段代码,他会须要一点时间才能完整理解这段代码,由于他没法直接看清代码的目标。

不过若是这里咱们使用DSL,也就是LINQ,就像这样:

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() });

products……先调用Where……blablabla……再GroupBy等等。因为咱们这里能够使用DSL来表示高阶的术语,用以体现咱们想作的事情。因而这段代码则更加关注于“What”而不是“How”。我这里不会明确地指示我想要过滤的方式,我也不会明确地说我要创建字典和分类,这样基础结构就能够聪明地,或者说更加聪明地去肯定具体的执行方式。你可能比较容易想到咱们能够并行地执行这段代码,由于我没有显式地指定作事方式,我只是表示出个人意图。

咱们打开页面……(打开页面)很显然咱们获得了相同的结果。

这里比较有趣的是,内部DSL是如何设计进C#语法中的,为此咱们为C# 3.0添加了一系列的特性,例如Lambda表达式,扩展方法,类型推断等等。这些特性统一块儿来以后,咱们就能够设计出更为丰富的API,组合以后便成为一种内部DSL,就像这里的LINQ查询语言。

除了使用API的形式以外,咱们还能够这样作:

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() };

编译器会简单地将这种形式转化为前一种形式。不过,这里我认为有意思的地方在于,你彻底能够建立一门和领域编程语言彻底无关的语法,而后等这种语法和API变得流行且丰富起来以后,再来创一种新的表现形式,就如这里的LINQ查询语法。我颇为中意这种语言设计的交流方式。

关于声明式编程的还有一部分重要的内容,那即是函数式编程。函数式编程已经有很长时间的历史了,当年LISP即是个函数式编程语言。除了LISP之外咱们还有其余许多函数式编程语言,如APLHaskellSchemeML等等。关于函数式编程在学术界已经有过许多研究了,在大约5到10年前许多人开始吸取和整理这些研究内容,想要把它们融入更为通用的编程语言。如今的编程语言,如C#、Python、Ruby、Scala等等,它们都受到了函数式编程语言的影响。

我想在这里先花几分钟时间简单介绍一下我眼中的函数式编程语言。我发现不少人据说过函数式编程语言,但还不十分清楚它们和普通的命令式编程语言究竟有什么区别。现在咱们在使用命令式编程语言写程序时,咱们常常会写这样的语句,嗨,x等于x加一,此时咱们大量依赖的是状态,可变的状态,或者说变量,它们的值能够随程序运行而改变

可变状态很是强大,但随之而来的即是叫作“反作用”的问题。在使用可变状态时,你的程序则会包含反作用,好比你会写一个无需参数的void方法,而后它会根据你的调用次数或是在哪一个线程上进行调用对程序产生影响,由于void方法会改变程序内部的状态,从而影响以后的运行效果。

而在函数式编程中则不会出现这个状况,由于全部的状态都是不可变的。你能够声明一个状态,可是不能改变这个状态。并且因为你没法改变它,因此在函数式编程中不须要变量。事实上对函数式编程的讨论更像是数学、公式,而不像是程序语句。若是你把x = x + 1这句话交给一个程序员看,他会说“啊,你在增长x的值”,而若是你把它交给一个数学家看,他会说“嗯,我知道这不是true”。

然而,若是你给他看这条语言,他会说“啊,y等于x加一,就是把x + 1的计算结果交给y,你是为这个计算指定了一个名字”。这时候在思考时就是另外一种方式了,这里y不是一个变量,它只是x + 1的名称,它不会改变,永远表明了x + 1。

因此在函数式编程语言中,当你写了一个函数,接受一些参数,那么当你调用这个函数时,影响函数调用的只是你传进去的参数,而你获得的也只是计算结果。在一个纯函数式编程语言中,函数在计算时不会对进行一些神奇的改变,它只会使用你给它的参数,而后返回结果。在函数式编程语言中,一个void方法是没有意义的,它惟一的做用只是让你的CPU发热,而不能给你任何东西,也不会有反作用。固然如今你可能会说,这个CPU发多少热也是一个反作用,好吧,不过咱们如今先不讨论这个问题。

这里的关键在于,你解决问题的方法和之前大不同了。我这里仍是用代码来讲明问题。使用函数式语言写没有反作用的代码,就比如在Java或C#中使用final或是readonly的成员。

例如这里,咱们有一个Point类,构造函数接受x和y,还有一个MoveBy方法,能够把一个点移动一些位置。 在传统的命令式编程中,咱们会改变Point实例的状态,这么作在平时可能不会有什么问题。可是,若是我把一个Point对象同时交给3个API使用,而后我修改了Point,那么如何才能告诉它们状态改变了呢?可能咱们能够使用事件,blablabla,若是咱们没有事件,那么就会出现那些不愉快的反作用了。

那么使用函数式编程的形式写代码,你的Point类仍是能够包含状态,例如x和y,不过它们是readonly的,一旦初始化之后就不能改变了。MoveBy方法不能改变Point对象,它只能建立一个新的Point对象并返回出来。这就是一个建立新Point对象的函数,不是吗?这样就可让调用者来决定是使用新的仍是旧的Point对象,但这里不会有产生反作用的状况出现。

在函数式编程里天然不会只有Point对象,例如咱们会有集合,如Dictionary,Map,List等等,它们都是不可变的。在函数式编程中,当咱们向一个List里添加元素时,咱们会获得一个新的List,它包含了新增的元素,但以前的List依然存在。因此这些数据结构的实现方式是有根本性区别的,它们的内部结构会设法让这类操做变的尽量高效。

在函数式编程中访问状态是十分安全的,由于状态不会改变,我能够把一个Point或List对象交给任意多的地方去访问,彻底不用担忧反作用。函数式编程的十分容易并行,由于我在运行时不会修改状态,所以不管多少线程在运行时均可以观察到正确的状态。两个函数彻底无关,所以它们是并行仍是顺序地执行便没有什么区别了。咱们还能够有延迟计算,能够进行Memorization,这些都是函数式编程中十分有趣的方面。

你可能会说,那么咱们为何不都用这种方法来写程序呢?嗯,最终,就像我以前说的那样,咱们不能只让CPU发热,咱们必需要把计算结果表现出来。那么咱们在屏幕上打印内容时,或者把数据写入文件或是Socket时,其实就产生了反作用。所以真实世界中的函数式编程,每每都是把纯粹的部分进行隔离,或是进行更细致的控制。事实上也不会有真正纯粹的函数式编程语言,它们都会带来必定的反作用或是命令式编程的能力。可是,它们默认是函数式的,例如在函数式编程语言中,全部东西默认都是不可变的,你必须作些额外的事情才能使用可变状态或是产生危险的反作用。此时你的编程观念便会有所不一样了。

咱们在本身的环境中开发出了这样一个函数式编程语言,F#,已经包含在VS 2010中了。F#诞生于微软剑桥研究院,由Don Syme提出,他在F#上已经工做了5到10年了。F#使用了另外一个函数式编程语言OCaml的常见核心部分,所以它是一个强类型语言,并支持一些如模式匹配,类型推断等现代函数式编程语言的特性。在此之上,F#又增长了异步工做流,度量单位等较为前沿的语言功能。

而F#最为重要的一点多是,在我看来,它是第一个和工业级的框架和工具集,如.NET和Visual Studio,有深刻集成的函数式编程语言。F#容许你使用整个.NET框架,它和C#也有相似的执行期特征,例如强类型,并且都会生成高效的代码等等。我想,如今应该是展现一些F#代码的时候了。

首先我想先从F#中我最喜欢的特性讲起,这是个F#命令行……(打开命令行窗口以及一个F#源文件)……F#包含了一个交互式的命令行,这容许你直接输入代码并执行。例如输入5……x等于5……而后x……显示出x的值是5。而后让sqr x等于x乘以x,因而我这里定义了一个简单的函数,名为sqr。因而咱们就能够计算sqr 5等于25,sqr 10等于100。

F#的使用方式十分动态,但事实上它是一个强类型的编程语言。咱们再来看看这里。这里我定义了一个计算平方和的函数sumSquares,它会遍历每一个列表中每一个元素,平方后再把它们相加。让我先用命令式的方式编写这个函数,再使用函数式的方式,这样你能够看出其中的区别。

let sumSquaresI l = 
    let mutable acc = 0
    for x in l do
        acc <- acc + sqr x
    acc

这里先是命令式的代码,咱们先建立一个累加器acc为0,而后遍历列表l,把平方加到acc中,而后最后我返回acc。有几件事情值得注意,首先为了建立一个可变的状态,我必须显式地使用mutable进行声明,在默认状况下这是不可变的。

还有一点,这段代码里我没有提供任何的类型信息。当我把鼠标停留在方法上时,就会显示sumSquaresI方法接受一个int序列做为参数并返回一个int。你可能会想int是哪里来的,嗯,它是由类型推断而来的。编译器从这里的0发现acc必须是一个int,因而它发现这里的加号表示两个int的相加,因而sqr函数返回的是个int,再接下来blablabla……最终它发现这里处处都是int。

若是我把这里修改成浮点数0.0,鼠标再停留一下,你就会发现这个函数接受和返回的类型都变成float了。因此这里的类型推断功能十分强大,也十分方便。

如今我能够选择这个函数,让它在命令行里执行,而后调用sumSquaresI,提供1到100的序列,就能获得结果了。

let rec sumSquaresF l = 
    match l with
    | [] -> 0
    | h :: t -> sqr h + sumSquaresF t

那么如今咱们来换一种函数式的风格。这里是另外一种写法,能够说是纯函数式的实现方式。若是你去理解这段代码,你会发现有很多数学的感受。这里我定义了sumSqauresF函数,输入一个l列表,而后使用下面的模式去匹配l。若是它为空,则结果为0,不然把列表匹配为头部和尾部,而后便将头部的平方和尾部的平方和相加。

你会发现,在计算时我不会去改变任何一个变量的值,我只是建立新的值。我这里会使用递归,就像在数学里咱们常用递归,把一个公式分解成几个变化的形式,以此进行递归的定义。在编程时咱们也使用递归的作法,而后编译器会设法帮咱们转化成尾递归或是循环等等。

因而咱们即可以执行sumSquaresF函数,也能够获得相同的结果。固然实际上可能你并不会像以前这样写代码,你可能会使用高阶函数:

let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )

例如这里,我只是把函数x乘以x映射到列表上,而后相加。这样也能够获得相同的结果,并且这多是更典型的作法。我这里只是想说明,这个语言在编程时可能会给你带来彻底不一样的感觉,虽然它的执行期特征和C#比较接近。


我下面继续要讲的是动态语言,这也是我以前提到的三种趋势之一。

我仍是尝试着去找到动态语言的定义,可是你也知道……通常地说,动态语言是一些不对编译时和运行时进行严格区分的语言。这不像一些静态编程语言,好比C#,你先进行编译,而后会获得一些编译期错误,稍后再执行,而对于动态语言来讲这两个阶段便混合在一块儿了。咱们都熟悉一些动态语言,好比JavaScript,Python,Ruby,LISP等等。

动态语言有一些优点,而静态语言也有着另外一些优点,这也是两个阵营争论多年的内容。老实讲,我认为结果不是二者中的任意一个,它们都有各自十分重要的优势,而长期来看,我认为结果应该是二者的杂交产物,我认为在语言发展中也能够看到这样的趋势,这两部份内容正在合并。

许多人认定动态语言执行起来很慢,也没有类型安全等等。我想在这里观察并比较一下,到底是什么缘由会让静态语言和动态语言在这方面有不一样的性质。这里有一段有趣的代码,它的语法在JavaScript和C#里都是正确的,这样咱们便能比较两种语言是如何处理这段代码的。

首先咱们把它看做是一段C#代码,它只是用for循环把一堆整数相加,你确定不会这么作,这只是一个示例。在C#中,当咱们使用var关键字时,它表示“请为我推断这里的类型”,因此在这里a和i的类型都是int。

这断代码在执行的时候,这两个值都是32位整数,而for循环只是简单的使用ADD指令便可,执行起来天然效率很高。

但若是从JavaScript或是动态语言的角度来看……或者说对于动态类型的语言来讲,var只表明了“一个值”,它能够是任意类型,咱们不知道它到底是什么。因此当咱们使用var a或var i时,咱们只是定义了两个值,其中包含了一个“类型”标记,代表在运行时它是个什么类型。在这里它是一个int,所以包含了存储int值的空间。但有些时候,例如要存储一个double值,那么可能便须要更多的空间,还多是一个字符串,因而便包含一个引用。

因此二者的区别之一即是,表示一样的值在动态语言中会有一些额外的开销,代价较高。而在现在的CPU中,“空间”便等于“速度”,因此较大的值便须要较长时间进行处理,这里便损失了一部分效率。

在JavaScript中,咱们若是要处理a加i,那么便不只仅是一个ADD指令。首先它必须查看两个变量中的类型标记,而后根据类型选择合适的相加操做。因而再去加载两个值,而后再进行加法操做。这里还须要进行越界检查,由于在JavaScript中一旦越界了便要使用double,等等。很明显在这里也有许多开销。通常来讲,动态语言是使用解释器来执行的,所以还有一些解释器须要的二进制码。你把这些开销所有加起来之后,便会发现执行代码时须要10倍到100倍的开销。

不过因为近几年来出现的一些动态虚拟机或引擎,目前这些状况改善了许多。比方说,这是传统的状况(上图左),如在IE 6或IE 7里使用的很是缓慢的解释器。目前的状况是,大部分的JavaScript引擎使用了JIT编译器(上图中),因而便省下了解释器的开销,这样性能损失便会减少至3到10倍。而在过去的两三年间,JIT编译器也变得愈来愈高效,浏览器中新一代的适应性JIT编译器(上图右),如TraceMonkeyV8,还有现在微软在IE 9中使用的Chakra引擎。这种适应性的JIT编译器使用了一部分有趣的技术,如Inline Caching、Type Specialization、Hidden Classes、Tracing等等,它们能够将开销下降至2到3倍的范围内,这种效率的提高可谓十分神奇。

在我看来,JavaScript引擎可能已经接近了性能优化的极限,咱们在效率上能够提高的空间已经很少。不过我一样认为,现在JavaScript语言的性能已经足够快了,彻底有能力统治Web客户端。

有人认为,JavaScript历来不是一种适合进行大规模编程的语言。现在也有一些有趣的工具,如Google Web Tookit,在微软Nikhil Kothari也建立了Script#,让你能够编写C#或Java代码,而后将代码编译成JavaScript,这就像是将JavaScript看成是一种中间语言。Google Wave的全部代码都用GWT写成,它的团队坚持认为用JavaScript不可能完成这样的工做,由于复杂度实在过高了。现在在这方面还有一些有趣的开发成果,我不清楚何时会结束。不过我认为,这些都不算是大规模的JavaScript开发方案,而编写C#或Java代码再生成JavaScript的方式也不能算是彻底正确的作法。咱们能够关注这方面的走向。

在.NET 4.0的运行时进行动态编程时,咱们引入了一个新功能:动态语言运行时。能够这样理解,CLR的目的是为静态类型的编程语言提供一个统一的框架或编程模型,而DLR即是在.NET平台上为动态语言提供了统一的编程模型。CLR自己已经有一些支持动态编程能力,如反射,Emit等等。不过在.NET上实现动态语言的时候,总会一遍又一遍地去实现某些功能,还有如动态语言如何与静态语言进行交互,这些都由DLR来提供。DLR的特性包含了,如表达式树、动态分发、Call Site缓存,这能够提升动态代码的执行效率。

在.NET 4.0中咱们使用了DLR,不只仅是IronPython和IronRuby,还有C# 4和VB.NET 10,它们使用DLR实现动态分发功能。所以咱们共享了语言的动态能力实现方式,因而这些语言之间能够轻松地进行交互。一样咱们能够与其余多样性的技术进行交互,例如使用JavaScript操做Silverlight的DOM,或是与Ruby、Python代码沟通,甚至用来控制Office等自动化服务。


动态语言的另外一个关键和有趣之处在于“元编程”。“元编程”其实是“代码生成”的一种别称,其实在平常应用中咱们也常常依赖这种作法。观察动态语言适合元编程的缘由也是件十分有趣的事情。

在这个蓝框中是一段Ruby on Rails代码(见上图)。简单地说,这里定义了一个Order类,继承了ActiveRecord,也定义了一些关系,如belongs_to和has_many关系。Ruby这种动态语言的关键之处,在于一切事物都是经过执行而获得的,包括类型声明。好比这里的类型申明执行了belongs_to和has_many方法的调用,执行belongs_to会截获一对多或一对一关系所须要的信息,所以在这里语言是在运行的时候,动态为自身生成了代码。

实现这点在动态语言里天然会更容易一些,由于它们没有编译期和执行期的区别。静态类型语言在这方面会比较困难。例如在C#或Java里使用ORM时,传统的作法是让代码生成器去观察数据库,生成一大堆代码,而后再编译,有些复杂。不过我时常想着去改善这一点。

其中一种作法,是咱们正在努力实现的“编译器即服务”,我如今先对它进行一些简单的介绍。传统的编译器像是一个黑盒,你在一端输入代码,而另外一端便会生成.NET程序集或是对象代码等等。而这个黑盒却很神秘,你目前很难参与或理解它的工做。

你能够想象,一些代码每每是不包含在源文件中的。若是你想要交互式编程的体验,例如一个交互式的提示符,那么代码不是保存在源文件中而是由用户输入的。若是您在实现一个DSL,例如Windows Workflow或是Biztalk,则可能用C#或VB实现了一些须要动态执行的规则,它们也不是保存在源文件中,而多是放在XML属性中的。此时你想编译它们却作不到,你仍是要把它们放入源文件,这就变的复杂了。

另外一方面,对于编译器来讲,咱们不必定须要它生成程序集,有时候须要的是一些树状的表现形式。例如一些由用户反射生成的代码,即可能不要程序集而是一个解析树,而后能够对它进行识别和重写。所以,咱们可能愈来愈须要的是一些API,以此开放编译器的功能。

例如,你能够给它一小段代码,让它返回一段可执行的程序,或是一个能够识别或重写的解析树。这么作可让静态类型语言得到许多有用的功能,例如元编程,以及可操做的完整的对象模型等等。


好,最后我想谈的内容是“并发”。

据说过摩尔定律的请举手……几乎是全部人。那么多少人据说了摩尔定律已经结束了呢?嗯,仍是有不少人。我有好消息,也有坏消息。我认为摩尔定律并无中止。摩尔定律说的是:能够在集成电路上低成本地放置晶体管的数目,约每两年便会增长一倍。有趣的是,这个定律从60年代持续到如今,而从一些迹象上来看,这个定律会继续保持20到30年。

摩尔定理有个推论,即是说时钟速度将根据相同的周期提升,也就是说每隔大约24个月,CPU的速度便会加倍──而这点已经中止了。再来统计一下,大家之中有谁的机器里有20GHz的CPU?看到了没?一我的都没有。但若是你从五年前开始计算的话,如今咱们应该已经在使用20GHz的CPU了,但事实并不是如此。这点在五年前就中止了,并且事实上最大速度还有些降低,由于发热量实在太大了,会消耗许多能源,让电池用的太快。

有些物理方面的基础因素让CPU不能运行的太快。然而,另外一意义上的摩尔定理出现了。咱们仍是能够看到容量的增长,由于能够在同一个表盘上放置多个CPU了。目前已经有了双核、四核,Intel的CTO在三年前说,十年后咱们能够出现80核的处理器。

到了那个时候,你的任务管理器中就多是这样的。彷佛有些吓人,不过这是咱们实验室中真实存在的128核机器。你能够看到,计算能力已经彻底用上了。这即是个问题,好比你在这台强大的机器上进行一个实验,你天然但愿看到100%的使用情况,不过传统的实验都是在一个核上执行的,因此咱们面临的挑战是,咱们须要换一种写程序的方式来利用此类机器。

个人一个同事,Herb Sutter,他写过一篇文章,谈到“免费的午饭已经结束了”。没错,咱们已经不能写一个程序,而后对客户说:啊,将来的硬件会让它运行的愈来愈快,咱们不用关心太多──不,已经不会这样了,除非你换种不一样的写法。实话说,这是个挑战,也是个机遇。说它是个挑战,是由于并发十分困难,至今咱们对此尚未简单的答案,稍后我会演示一些正有所改善的东西,但……这也是一个机遇,在这样的机器上,你的确能够用完全部的核,这样便能得到性能提升,不过作法须要有所不一样。

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

使用目前的并发API来完成工做并不容易,好比使用Thread,ThreadPool,lock,Monitor等等,你没法太好的进展。不过.NET 4.0提供了一些美妙的事物,咱们称之为.NET并行扩展。它是一种现代的并发模型,将逻辑上的任务并发与咱们实际使用的的物理模型分离开来。之前咱们的API都是直接处理线程,也就是(上图)下方橙色的部分,不过有了.NET并行扩展以后,你能够使用更为逻辑化的编程风格。任务并行库(Task Parallel Library),并行LINQ(Parallel LINQ)以及协调数据结构(Coordination Data Structures)让你能够直接关注逻辑上的任务,而没必要关心它们是如何运行的,或是使用了多少个线程和CPU等等。

下面我来简单演示一下它们的使用方式。我带来了一个PLINQ演示,这里是一些代码,读取XML文件的内容。这有个50M大小的popname.xml文件,保存了美国社会安全数据库里的信息,包含某个洲在某一年的人口统计信息。这个程序会读取这个XML文件,把它转化成一系列对象,并存放在一个List中。而后对其执行一个LINQ语句,查找全部在华盛顿名叫Robert的人,再根据年份进行排序:

Console.WriteLine("Loading XML data...");
var popNames =
    (from e in XElement.Load("popnames.xml").Elements("Name")
     select new
     {
         Name = (string)e.Attribute("Name"),
         State = (string)e.Attribute("State"),
         Year = (int)e.Attribute("Year"),
         Count = (int)e.Attribute("Count")
     })
    .ToList();

Console.WriteLine(popNames.Count + " records");
Console.WriteLine();

string targetName = "Robert";
string targetState = "WA";

var querySequential =
    from n in popNames
    where n.Name == targetName && n.State == targetState
    orderby n.Year
    select n;

咱们来执行一下……首先加载XML文件,而后进行查询。利用PLINQ咱们能够作到并行地查询。咱们只要拷贝一份代码……改为queryParallel……如今我惟一要作的只是在数据源上使用AsParallel扩展方法,这样便会引入一套新的类型和实现,此时相同的LINQ操做使用的即是并行的实现:

var queryParallel =
    from n in popNames.AsParallel()
    where n.Name == targetName && n.State == targetState
    orderby n.Year
    select n;

咱们从新执行两个查询。

再次加载XML数据……并行实现使用了1.5秒,咱们再试着运行一次,通常结果会更好一些,如今可能恰好在执行一些后台任务。通常咱们能够获得更快的结果……此次比较接近了。如今你能够观察到,咱们并不须要作太多事情,即可以在个人双核机器上获得并发的效果。

这里我没法保证说,咱们只要随时加上AsParallel即可以获得两倍的性能,有时能够有时不行,有些查询可以被并行,有的则不能够。然而,我想你必定赞成一点,使用如LINQ这样的DSL可以方便咱们编写并行的代码,也更有可能利用起并行效果。虽然不是每次都有效,可是尝试的成本也很低。若是咱们使用普通的for循环来编写代码,在某个地方使用线程池等等,便很容易在这些API里失去方向。而这里咱们只要简单地尝试一下,便能知道是否能够提升性能了。

这里你已经看到我使用的LINQ查询,而如今也有不少工做是经过循环来完成的。你能够想象主要的运算是从哪里来的,很天然会是在循环里操做数据。若是循环的每一个迭代都是独立的,便有很大的机会能够利用并发操做──我知道这里是“若是”,不过长期来看则必定会出现这样的状况。这时候即可以使用并行扩展,或者说是.NET并行扩展里的新API,把循环转化成并行的循环,只要简单的改变……几乎只要用一样的循环体把for重构成Parallel.For就好了。若是你有foreach操做就能够使用Parallel.ForEach,或是一系列顺序执行的语句也能够用上Parallel.Invoke。此时任务并行库会接管并执行这些任务,根据你的CPU数量使用最优化的线程数量,你不须要关注更深的细节,只须要编写逻辑就能够了。

就像我说的那样,可能你会有独立的任务但也可能没有,因此不少时候咱们须要编程语言来关注这方面的事情。好比“隔离性(Isolation)”。例如,编译器如何发现这段代码是独立的,能够安全地并发执行,比如我建立了一个对象,在分享给其余人以前,我对它的改变是安全的。可是我一旦把它们共享出去了,那么它们便不安全了。因此若是咱们的类型系统能够跟踪到这样的共享,如Linear Types──这在学术界也有一些研究。咱们也能够在函数的纯洁性(Purity)方面下功夫,如关注某个函数是否有反作用,有些时候编译器能够作这方面的检查,它能够禁止某些操做,以此保证咱们写出纯函数。还有即是不可变性(Immutability),目前的C#或VB,咱们须要额外的工做才能写出不可变的代码──但本不应这样,咱们应该在语言层面上更好的支持不可变性。这些都是在并发方面须要考虑的问题。

若是说有哪一个语言特性超出这个范畴,我想说这里还有一个原则:你不应指望C#中出现某个特别的并发模型,而应该是一种通用的,可用于各类不一样的并发场景的特性,就像隔离性、纯洁性及不可变性那样。语言拥有这样的特性以后,就能够用于构建各类不一样的API,各类并发方式均可以利用到核心的语言特性。


OK,我想如今已经讲的差很少了,我来作个总结吧。

在我看来,对于编程语言来讲,如今出现了许多有趣的东西,也是使人激动的时刻。在过去,大约1995-2005年,的确能够说是一个有些特别的编程语言的黄金时期。你知道,当Java出现的时候,编程语言的门槛变得平坦了,一切都是Java,天啊其余编程语言都完蛋了,咱们也没什么可作的了。而后咱们又逐渐发现,这远没有结束,如今回顾起来,会发现又出现了许多有趣的编程语言。我很兴奋,由于新语言表明了咱们在编程领域上的进步。

若是要我归纳在将来十年编程语言会变成什么样,首先,我认为编程语言应该变得更加“声明式”,咱们须要设法为语言引入一些如元编程,函数式编程的能力,同时可能也要寻找让用户有办法扩展语法,使他们能够构造领域特定语言等等。我想在十年之后,动态语言和静态语言的区别也差很少会消失了,这二者会合并为一种单一的常见的编程范式。在并发方面,语言会采纳一些特性,能够利用起隔离性,函数式的纯粹性,以及更好的不可变数据类型的编写方式。不过整体来讲我想强调的是,对于编程语言,新的范式则是“多范式”编程语言。

相关文章
相关标签/搜索