编程语言的历史早于真正意义的计算机的出现。19世纪就有"可编程的"织布机和钢琴弹奏装置出现,它们都是领域特定语言(DSL)的样例。
编程语言发展的编年史
我们一个统计出来256种编程语言,当然,这么多的语言中只有一些是常用的或实用的。
1951 – Regional Assembly Language
1952 – Autocode
1954 – IPL (LISP语言的祖先)
1955 – FLOW-MATIC (COBOL语言的祖先)
1957 – FORTRAN (第一个编译型语言)
1957 – COMTRAN (COBOL语言的祖先)
1958 – LISP
1958 – ALGOL 58
1959 – FACT (COBOL语言的祖先)
1959 – COBOL
1959 – RPG
1962 – APL
1962 – Simula
1962 – SNOBOL
1963 – CPL (C语言的祖先)
1964 – BASIC
1964 – PL/I
1966 – JOSS
1967 – BCPL (C语言的祖先)
1968 – Logo
1969 – B (C语言的祖先)
1970 – Pascal
1970 – Forth
1972 – C
1972 – Smalltalk
1972 – Prolog
1973 – ML
1975 – Scheme
1978 – SQL
1980 – C++ (既有类的C语言,更名于1983年7月)
1983 – Ada
1984 – Common Lisp
1984 – MATLAB
1985 – Eiffel
1986 – Objective-C
1986 – Erlang
1987 – Perl
1988 – Tcl
1988 – Mathematica
1989 – FL
1990 – Haskell
1991 – Python
1991 – Visual Basic
1993 – Ruby
1993 – Lua
1994 – CLOS (ANSI Common Lisp的一部分)
1995 – Java
1995 – Delphi (Object Pascal)
1995 – Java
1995 – PHP
1996 – WebDNA
1997 – Rebol
1999 – D
2000 – Action
2001 – C#
2001 – Visual Basic .NET
2002 – F#
2003 – Groovy
2003 – Scala
2007 – Clojure
2009 – Go
2011 – Dart
从1951年2014年,人类一共发明了256种编程语言,每一种语言的出现都带有某些新特征。编程语言不断的在革新,很快就会有超出这个清单的新编程语言出现
1801- Joseph Marie Jacquard用打孔卡为一台织布机编写指令,在挂毯上织出了“hello, world”字样。当时的reddit网友对这项工作的反响并不热烈,因为它既缺少尾递归调用,又不支持并发,而且甚至都没有注意在拼写时恰当地区分大小写。
-
Jacquard织布机是第一台可进行程序控制的织布机。用打孔卡进行编程的概念,直到电子计算机被发明出来之后仍然被广泛运用。
-
最早的(出自K&R C)打印的是全小写的字符串:"hello, world"。
-
在许多英文技术社区里,不正确地使用大小写发贴会被视作是小白的行为。(如把“Python”拼作“python”,把“FreeBSD”拼作“freebsd”,把“Qt”拼作“QT”)
1842- Ada Lovelace写了世界上第一个程序。她的努力只遇到了一点点小小的麻烦,那就是:实际上并没有任何计算机能够用来运行她的程序。后来的企业架构师们重新吸收了她的这个技能,用来学习如何更好地使用UML进行编程。
-
Ada Lovelace为Charles Babbage的分析机写了一个计算伯努利数的算法实现,因此被后世公认为是世界上第一个程序员。实际上,分析机由于其设计思想过于先进,在当时根本没有 被制造出来。(Babbage的分析机一般被认为是现代电子通用计算机的先驱)
-
讽刺现在的某些“软件架构师”顶多只会纸上谈兵地画画UML。
1936- Alan Turing发明了世间一切程序语言的最终形态,但很快他就被英国军情六处“请”去当007了,以至于他根本来不及为这些语言申请专利。
-
与通用图灵机(Universal Turing machine)等价的语言被称为图灵完备的(Turing completeness),它定义了“什么样的语言可以被称作是程序语言”。
-
二战期间Turing曾秘密地为英国军方工作,破解德军的Enigma密码机,并在战后被授予大英帝国勋章。但这项事实直到多年以后才向公众公开。
1936- Alonzo Church同时也发明了世间一切程序语言的最终形态,甚至做得更好。但他的λ演算被绝大部分人忽视了,因为它与C语言“不够像”。尽管存在着这样的批评,但事实上,C在当时还没有被发明出来。
-
Church是Turing在Princeton的博士生导师,他在λ演算方面的工作先于Turing指出了不存在一个对可判定性问题的通用解法,这后来证明和Turing针对停机问题提出的图灵机模型是等价的。即著名的。
-
说Church“甚至做得更好”,因为λ演算为后世所有的函数式语言提供了理论基础。
-
现在一种常见的关于函数式编程的批评就是:“它们与C语言不够像”。
1940年代- 一些直接采用布线和开关来进行程序控制的“计算机”出现了。工程师们当时这么做,据说是为了避开“用空格还是用制表符缩进”这样的论战。
-
据说当时负责设计ENIAC的工程师中间曾经发生过这样的争论:
-
空格比制表符好。
-
制表符比空格好。
-
4个空格比8个空格好。
-
什么?用2个空格的统统烧死。
-
-
关于这台具有里程碑意义的人类史上第一台电子计算机ENIAC上应该预装何种编辑器,工程师们还发生过这样的争吵:
-
Vim比Emacs好!
-
Emacs比Vim好!
-
强烈推荐Sublime Text。
-
你丫用编辑器的都是找虐,IDE才是王道。
-
没错,要用就用世界上最好的公司微软开发出来的世界上最好的IDE:Visual Studio。
-
我早就看透了无谓的编辑器论战什么的了,我要告诉楼上吵架的,你们全都是傻逼!
-
-
最后,工程师们一致决定使用布线和开关来为他们即将发明的计算机进行编程,机智地避开了所有这些无谓的争吵,最终齐心协力创造出了人类历史上第一台电子计算机:ENIAC。(鼓掌
-
(图:两位ENIAC程序员在运用敏捷开发方法进行愉快的结对编程。“自从抛弃伴随我多年的Emacs和HHKB Pro、改用布线和开关进行编程之后,我的左手小指麻痹奇迹般地痊愈了。”其中一位接受采访时如是说。另一位则表示:“新的编程方式让曾经专注颈椎病20 年的我得到了彻底的康复,不用再整天盯着显示屏,身心同时得到了极大的放松,值得大力推广!”)
1957- John Backus和IBM发明了FORTRAN语言。关于IBM或FORTRAN并没有什么特别好笑的地方。除了,写FORTRAN程序的时候不系蓝领带将被编译器视作是一个syntax error。
-
蓝领带、白衬衫、深色西装似乎是IBM公司20世纪经典的dress code。
-
早期FORTRAN(FORTRAN 77)对程序书写格式的要求那是相当严格。(例如,蛋疼的固定格式缩进)
1958- John McCarthy和Paul Graham发明了LISP。由于冷战期间的战略括号资源储备所造成的巨大成本,LISP从未流行过。尽管欠缺足够的流行度,LISP(现在叫做 “Lisp”,有时叫“Arc”)仍然被视作一门有影响力的语言,在关键的算法思想诸如递归(recursion)和提升逼格 (condescension)上尤为典范。
-
LISP发明的那一年Paul Graham其实还没有出生。据说是因为某本叫做《Haste and Waste》的伪程装黑圣典实在太有名了,以至于许多编程小白们把写这本书的传奇人物同Lisp之间画上了等号。
-
提升逼格确实是一种与递归调用同样关键的算法思想。嗯,你懂的。
1959- 在输掉了和L. Ron Hubbard之间的一场打赌之后,Grace Hopper和其他几个抖S发明了所谓的“面向Boilerplate的全大写化语言(Capitalization Of Boilerplate Oriented Language,COBOL)”。多年以后,由于一些被误导的、性别歧视主义者对Adm. Hopper关于COBOL的工作的报复,在Ruby技术会议上不时会看到一些厌女主义乃至仇视女性的材料出现。
-
L. Ron Hubbard是山达基教(Scientology)的创始人,二战期间曾与Grace Hopper同样供职于美国海军。(尚不清楚这两人之间有无其他联系)
-
COBOL语言以代码极其冗长和通篇大写字母的书写风格而闻名。
-
Adm. Hopper:Grace Murray Hopper女士的军衔是Rear Admiral Lower Half,即美国海军准将。
1964- John Kemeny和Thomas Kurtz创造了BASIC,一个为非计算机科学家设计的非结构化的程序语言。
1965- Kemeny和Kurtz两人goto到了1964。
-
调侃BASIC语言对行号和goto的无节制滥用。
1970- Guy Steele和Gerald Sussman创造了Scheme。他们的工作导致了一系列以《Lambda之究极(Lambda the Ultimate)……》为标题开头的论文发表,并在《Lambda之究极厨房神器》这一篇中达到了最高潮。以这篇论文为基础,开始了一个长年累月的、收 视率究极失败的晚间电视购物节目。Lambda们因为其概念相对难以理解而被大众所忽视,直到未来的某一天,Java语言终于让它们变得有名了起来。通过 不包含它们这件事情。
1970- Niklaus Wirth创造了Pascal,一个过程式的语言。很快就有人开始声讨Pascal,因为它使用了类似“x := x + y”这样的语法,而不是更为人熟知的类C语法“x = x + y”。尽管存在着这样的批评,而事实上当时C还没有被发明出来。
1972- Dennis Ritchie发明了一把射击时能同时向前和向后两个方向发射子弹的绝世好枪。但他对此发明造成的致死和终身残疾数量感到还不够满意,所以他又发明了C语言和Unix。
-
翻译君:……
1972- Alain Colmerauer设计了逻辑编程语言Prolog。他的目标是创造一个具有两岁小孩智商的程序语言。为了证明他成功达到了这个目标,他展示了一个Prolog程序,它对于每条查询都会机智地给出相同的回答:“No”。
1973- Robin Milner创造了ML,一个建立在M&M类型理论基础上的语言。由ML衍生而来的SML加上了一套形式语义的规范。当被要求给这个形式语义本身 书写一套形式语义时,Milner的脑子爆掉了。其他ML家族的著名语言还包括OCaml,F#,和,Visual Basic。
1980- Alan Kay创造了Smalltalk并发明了“面向对象”这个词。当被问到它的含义时,他回答道:“Smalltalk程序本身就是对象。”当被问到对象是由 什么组成时,他回答到:“对象。”当再一次被问到这个问题时,他说“看,它从里到外都是对象。直到你抽出一只乌龟。”
-
Smalltalk的设计从很大程度上受到了的影响。
1983- 为了纪念伟大的先辈程序员Ada Lovelace那能够写出永远也无法被执行的代码的彪悍技能,Jean Ichbiah和美国国防部创造了Ada语言。尽管缺乏证据显示有任何重要的Ada程序曾经被完成过,历史学家仍然确信Ada是个成功的公益项目,它让数 以千计的国防承包商免于沦落为与黑帮为伍。
-
Ada曾经是美国国防部指定的嵌入式计算机系统唯一开发语言,在其研发上耗资巨大。(国防承包商们于是不用靠贩卖军火给黑帮来维持生计了)
-
虽然Ada的整型范围溢出检查失败导致弄坏了欧空局的一枚Ariane 5运载火箭,不过美国国防部发言人对此表示:关我P事。
1983- Bjarne Stroustrup把他所听说过的一切都试图嫁接到C上,创造出了C++。最后得到的语言是如此地复杂,以至于程序必须被送到未来去让“天网”人工智能 进行编译。编译时间难以容忍。天网开展这项服务的动机仍然不为人知,但来自未来的发言人说道:“没什么好担心的,宝贝。”带着一口奥地利腔的机械口音。有 一些来自坊间的推测,所谓的天网只不过是个自命不凡的缓冲区溢出而已。
1986- Brad Cox和Tom Love创造了Objective-C,宣称“该语言完美地结合了C的内存安全性与Smalltalk的神奇效率”。现在的历史学家怀疑这两人其实是诵读障碍症患者。
-
“C的内存安全性十分好”。
-
Smalltalk编译出来的程序以低效缓慢著称。
1987- Larry Wall在电脑前打了个盹,Larry Wall的脑门子压到了键盘上。醒来之后,Larry Wall深信 ,在Larry Wall的显示器上出现的神秘字符串并非是随机的,那是某种编程语言之程序样例的神谕。那必是上帝要他的先知,Larry Wall,去设计的。Perl语言就此诞生了。
1990- 一个由Simon Peyton-Jones、Paul Hudak、Philip Wadler、Ashton Kutcher和善待动物组织(PETA)组成的委员会创造了Haskell,一种纯函数式的、非严求值的语言。Haskell由于使用了Monad这种 较费解的概念来控制副作用而遭到了一些批评意见。Wadler试图平息这些质疑,他解释说:“一个单子(Monad)说白了不过就是自函子范畴上的一个幺 半群而已,这有什么难以理解的?”
1991- 荷兰程序员Guido van Rossum为了一次神秘的手术而进行了一次阿根廷之旅。回来后他带着一个巨大的颅疤,发明了Python,而被数以军团计的追随者们加冕为“终生大独裁 者”,并向全世界宣布“要办到一件事情,只可有唯一的一种方法!”。整个波兰陷入了恐慌。
-
:开源社区一种流行的说法,“仁慈的”终生大独裁者。这个说法最早指的就是Guido van Rossum。
-
希特勒在提出建立“纯正的雅利安人国家”“统一的大德意志帝国”并实现了德奥合并之后,翌年便入侵了波兰,引发了第二次世界大战。“我一个人征服了整个欧洲!”(感觉好棒好棒的)
1995- 在家门口附近的一个意大利饭馆用餐时,Rasmus Lerdorf意识到他吃的那盘意面正好是一个用来理解WWW万维网的极好模型,而所有的Web应用都应该仿照它们的媒介那样去做。在他的餐巾的背后,他 设计出了著名的“可编程超链接Pasta(Programmable Hyperlinked Pasta,PHP)”语言。PHP的文档至今仍然保留在那片餐巾上。
-
PHP最显著的特点就是:代码是可以直接嵌在HTML文档中的。
1995- 松本“Mad Matz”行弘创造出了Ruby语言,用来辟谣一些意味不明的、有关澳洲将会变成一片由莫霍克族战士和Tina Turner统治的荒漠的末世预言。该语言后来被它的真正发明者David Heinemeier Hansson重新命名为Ruby on Rails。(关于某个叫松本行弘的人发明了一种叫做Ruby的语言这件事情从未发生过,最好在这篇文章的下一个版本中删掉。- DHH表示)
-
最早关于Ruby的国际会议不是RubyConf,而是每年在澳大利亚举办的OSDConf。
-
这里应该是在吐槽Ruby的杀手级应用Ruby on Rails实在太有名了,以至于超越了原来的Ruby语言本身。
-
Matz并没有为“Ruby”这个名字注册商标——本着开源的黑客精神。
-
而DHH(RoR的作者)却把“Ruby on Rails”这个(包含了“Ruby”字样的)名称注册成了商标,并且阻止别人未经授权使用“Rails”这个名字。
-
(虽然抢注商标对开源来说未必是一件坏事情——Python基金会今年在欧洲还卷入了一场商标之争了不是)
-
假如你从来没听说过的话,莫霍克族战士据说是像这个样子的:
-
而看起来是这个样子的:
1995- Brendan Eich读完了历史上所有在程序语言设计中曾经出现过的错误,自己又发明了一些更多的错误,然后用它们创造出了Live。之后,为了紧跟 Java语言的时髦潮流,它被重新命名为Java。再然后,为了追随一种皮肤病的时髦潮流,这语言又被命名为ECMA。
1996- James Gosling发明了Java。Java是一个相对繁冗的、带垃圾收集的、基于类的、静态类型的、单分派的面向对象语言,拥有单实现继承和多接口继承。Sun不遗余力地宣传着Java的独一无二不同凡响之处。
2001- Anders Hejlsberg发明了C#。C#是一个相对繁冗的、带垃圾收集的、基于类的、静态类型的、单分派的面向对象语言,拥有单实现继承和多接口继承。微软不遗余力地宣传着C#的独一无二不同凡响之处。
2003- 一个叫Martin Odersky的醉汉看见了好时瑞森花生酱杯的广告,展示了某个人的花生酱倒入另一个人的巧克力的场景,他忽然有了个点子。他创造了Scala,一种结合 了面向对象和函数式编程的语言。这同时激怒了两个阵营的忠实信徒,他们立刻宣布要发动圣战烧死异教徒。
程序设计离不开编程语言,但是编程语言在国内的大环境中似乎一直是个二等公民。国内的计算机教育和工程培训,似乎一直在宣传“语言不重要,重要的是思想”,“语言一通百通”等观点,甚至在许多人眼中“语言的讨论”完全是不入流的,但其实“编程语言”与“工具”、“框架”或是“开发方法”等事物一样,都对生产力有着重要的影响。事实上,语言的发展历史比其他方面更为悠久,并且在过去十几年,甚至最近几年中都依然在不断的碰撞,演变。期间一些新的语言诞生了,而另一些在当时看来阳春白雪的语言和编程范式也重新获得了人们的重视。
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包含了无数强大的功能,例如语法提示,重构,调试器等等。与此相比,编程语言的改进的确很不明显。
在过去5、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的产品,并对所属种类进行分组,并降序地列出每组的分类名称及产品数量。如果是用命令式的编程方式,则可能是这样的:
- 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,代码便可以改写成:
- 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查询语句来改写:
- 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#中如果要计算一个列表所有元素之和,也可以使用命令式的风格来编写代码:
- let sumSquaresI l =
- let mutable acc = 0
- for x in l do
- acc <- acc + sqr x
- acc
acc只不过,F#中的一切默认都是不可变的,开发人员需要使用mutable关键字来声明一个可变的状态。事实上,在F#中更典型做法是:
- let rec sumSquaresF l =
- match l with
- | [] -> 0
- | head :: tail -> sqr head + sumSquaresF tail
在数学里我们经常使用递归,把一个公式分解成几个变化的形式,以此进行递归的定义。纯函数式的代码其“数学性”较强,如果您分析上面这段代码,会发现它几乎就是标准的数学定义。在编程时我们也使用递归的做法,编译器会设法帮我们转化成尾调用或是循环语句。
动态语言与元编程
动态语言不会严格区分“编译时”和“运行时”。对于一些静态编程语言(如C#),往往是先进行编译,此时可能会得到一些编译期错误,而对于动态语言来说这两个阶段便混合在一起了。常见的动态语言有JavaScript,Python,Ruby,LISP等等。动态语言和静态语言各有一些优势,这也是两个阵营争论多年的内容。不过Anders认为它们各自都有十分重要的优点,而未来不属于其中任何一方。他表示,从编程语言发展过程中可以观察到两种特点正在合并的趋势,未来应该属于两者的杂交产物。
许多人认定动态语言执行起来很慢,也没有类型安全等等。例如有这样一段代码:
- 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,各种并发方式都可以利用到核心的语言特性。
参考:
https://www.sohu.com/a/136310616_505803
https://blog.csdn.net/u010178308/article/details/79001888