Ⅰ 如何设计一门语言
为什么要设计一门新语言?原因无非就两个,要么旧的语言实在是让人受不了,要么是针对领域设计的专用语言。后一种我就不讲了,因为如果没有具体的领域知识的话,这种东西永远都做不好(譬如SQL永远不可能出自一个数据库很烂的人手里),基本上这不是什么语言设计的问题。所以这个系列只会针对前一种情况——也就是设计一门通用的语言。通用的语言其实也有自己的“领域”,只是太多了,所以被淡化了。纵观历史,你让一个只做过少量的领域的人去设计一门语言,如果他没有受过程序设计语言理论的系统教育,那只能做出屎。譬如说go就是其中一个——虽然他爹很牛逼,但反正不包含“设计语言”这个事情。
因此,在21世纪你还要做一门语言,无非就是对所有的通用语言都不满意,所以你想自己做一个。不满意体现在什么方面?譬如说C#的原因可能就是他爹不够帅啦,譬如说C++的原因可能就是自己智商太低hold不住啦,譬如说Haskell的原因可能就是用的人太少招不到人啦,譬如说C的原因可能就是实在是无法完成人和抽象所以没有linus的水平的人都会把C语言写成屎但是你又招不到linus啦,总之有各种各样的原因。不过排除使用者的智商因素来讲,其实有几个语言我还是很欣赏的——C++、C#、Haskell、Rust和Ruby。如果要我给全世界的语言排名,前五名反正是这五个,虽然他们之间可能很难决出胜负。不过就算如此,其实这些语言也有一些让我不爽的地方,让我一直很想做一个新的语言(来给自己用(?)),证据就是——“看我的博客”。
那么。一个好的语言的好,体现在什么方面呢?一直以来,人们都觉得,只有库好用,语言才会好用。其实这完全是颠倒了因果关系,如果没有好用的语法,怎么能写出好用的库呢?要找例子也很简单,只要比较一下Java和C#就够了。C#的库之所以好用,跟他语言的表达能力强是分不开的,譬如说linq(,to xml,to sql,to parser,etc),譬如说WCF(仅考虑易用性部分),譬如说WPF。Java能写得出来这些库吗?硬要写还是可以写的,但是你会发现你无论如何都没办法把他们做到用起来很顺手的样子,其实这都是因为Java的语法垃圾造成的。这个时候可以抬头看一看我上面列出来的五种语言,他们的特点都是——因为语法的原因,库用起来特别爽。
当然,这并不要求所有的人都应该把语言学习到可以去写库。程序员的分布也是跟金字塔的结构一样的,库让少数人去写就好了,大多数人尽管用,也不用学那么多,除非你们想成为写库的那些。不过最近有一个很不好的风气,就是有些人觉得一个语言难到自己无法【轻松】成为写库的人,就开始说他这里不好那里不好了,具体都是谁我就不点名了,大家都知道,呵呵呵。
好的语言,除了库写起来又容易又好用以外,还有两个重要的特点:容易学,容易分析。关于容易学这一点,其实不是说,你随便看一看就能学会,而是说,只要你掌握了门道,很多未知的特性你都可以猜中。这就有一个语法的一致性问题在里面了。语法的一致性问题,是一个很容易让人忽略的问题,因为所有因为语法的一致性不好而引发的错误,原因都特别的隐晦,很难一眼看出来。这里我为了让大家可以建立起这个概念,我来举几个例子。
第一个例子是我们喜闻乐见的C语言的指针变量定义啦:
int a, *b, **c;
相信很多人都被这种东西坑过,所以很多教科书都告诉我们,当定义一个变量的时候,类型最后的那些星号都要写在变量前面,避免让人误解。所以很多人都会想,为什么要设计成这样呢,这明显就是挖个坑让人往下跳嘛。但是在实际上,这是一个语法的一致性好的例子,至于为什么他是个坑,问题在别的地方。
我们都知道,当一个变量b是一个指向int的指针的时候,*b的结果就是一个int。定义一个变量int a;也等于在说“定义a是一个int”。那我们来看上面那个变量声明:int *b;。这究竟是在说什么呢?其实真正的意思是“定义*b是一个int”。这种“定义和使用相一致”的方法其实正是我们要推崇的。C语言的函数定义参数用逗号分隔,调用的时候也用逗号分隔,这是好的。Pascal语言的函数定义参数用分号分隔,调用的时候用逗号分隔,这个一致性就少了一点。
看到这里你可能会说,你怎么知道C语言他爹就是这么想的呢?我自己觉得如果他不是这么想的估计也不会差到哪里去,因为还有下面一个例子:
int F(int a, int b);
int (*f)(int a, int b);
这也是一个“定义和使用相一致”的例子。就第一行代码来说,我们要如何看待“int F(int a, int b);”这个写法呢?其实跟上面一样,他说的是“定义F(a, b)的结果为int”。至于a和b是什么,他也告诉你:定义a为int,b也为int。所以等价的,下面这一行也是“定义(*f)(a, b)的结果为int”。函数类型其实也是可以不写参数名的,不过我们还是鼓励把参数名写进去,这样Visual Studio的intellisense会让你在敲“(”的时候把参数名给你列出来,你看到了提示,有时候就不需要回去翻源代码了。
关于C语言的“定义和使用相一致”还有最后一个例子,这个例子也是很美妙的:
int a;
typedef int a;
int (*f)(int a, int b);
typedef int (*f)(int a, int b);
typedef是这样的一个关键字:他把一个符号从变量给修改成了类型。所以每当你需要给一个类型名一个名字的时候,就先想一想,怎么定义一个这个类型的变量,写出来之后往前面加个typedef,事情就完成了。
不过说实话,就一致性来讲,C语言也就到此为止了。至于说为什么,因为上面这几条看起来很美好的“定义和使用相一致”的规则是不能组合的,譬如说看下面这一行代码:
typedef int(__stdcall*f[10])(int(*a)(int, int));
这究竟是个什么东西呢,谁看得清楚呀!而且这也没办法用上面的方法来解释了。究其原因,就是C语言采用的这种“定义和使用相一致”的手法刚好是一种解方程的手法。譬如说int *b;定义了“*b是int”,那b是什么呢,我们看到了之后,都得想一想。人类的直觉是有话直说开门见山,所以如果我们知道int*是int的指针,那么int* b也就很清楚了——“b是int的指针”。
因为C语言的这种做法违反了人类的直觉,所以这条本来很好的原则,采用了错误的方法来实现,结果就导致了“坑”的出现。因为大家都习惯“int* a;”,然后C语言告诉大家其实正确的做法是“int *a;”,那么当你接连的出现两三个变量的时候,问题就来了,你就掉坑里去了。
这个时候我们再回头看一看上面那一段长长的函数指针数组变量的声明,会发现其实在这种时候,C语言还是希望你把它看成“int* b;”的这种形式的:f是一个数组,数组返回了一个函数指针,函数返回int,函数的参数是int(*a)(int, int)所以他还是一个函数指针。
我们为什么会觉得C语言在这一个知识点上特别的难学,就是因为他同时混用了两种原则来设计语法。那你说好的设计是什么呢?让我们来看看一些其它的语言的作法:
C++:
function<int __stdcall(function<int(int, int)>)> f[10];
C#:
Func<Func<int, int, int>, int>[] f;
Haskell:
f :: [(int->int->int)->int]
Pascal:
var f : array[0..9] of function(a : function(x : integer; y : integer):integer):integer;
这些语言的做法,虽然并没有遵守“定义和使用相一致”的原则,但是他们比C语言好的地方在于,他们只采用一种原则——这就比好的和坏的混在一起要强多了(这一点go也是,做得比C语言更糟糕)。
当然,上面这个说法对Haskell来说其实并不公平。Haskell是一种带有完全类型推导的语言,他不认为类型声明是声明的一部分,他把类型声明当成是“提示”的一部分。所以实际上当你真的需要一个这种复杂结构的函数的时候,实际上你并不会真的去把它的类型写出来,而是通过写一个正确的函数体,然后让Haskell编译器帮你推导出正确的类型。我来举个例子:
superApply fs x = (foldr id (.) fs) x
关于foldr有一个很好的理解方法,譬如说foldr 0 (+) [1,2,3,4]说的就是1 + (2 + (3 + (4 + 0)))。而(.)其实是一个把两个函数合并成一个的函数:f (.) g = \x->f(g( x ))。所以上述代码的意思就是,如果我有下面的三个函数:
add1 x = x + 1
mul2 x = x * 2
sqr x = x * x
那么当我写下下面的代码的时候:
superApply [sqr, mul2, add1] 1
的时候,他做的其实是sqr(mul2(add1(1)) = ((1+1)*2) * ((1+1)*2) = 16。当然,Haskell还可以写得更直白:
superApply [(\x->x*x), (*2), (+1)] 1
Haskell代码的简洁程度真是丧心病狂啊,因为如果我们要用C++来写出对应的东西的话(C语言的参数无法是一个带长度的数组类型所以其实是写不出等价的东西的),会变成下面这个样子:
template<typename T>
T SuperApply(const vector<function<T(T)>>& fs, const T& x)
{
T result = x;
for(int i=fs.size()-1; i>=0; i--)
{
result = fs[i](result);
}
return result;
}
C++不仅要把每一个步骤写得很清楚,而且还要把类型描述出来,整个代码就变得特别的混乱。除此之外,C++还没办法跟Haskell一样吧三个函数直接搞成一个vector然后送进这个SuperApply里面直接调用。当然有人会说,这还不是因为Haskell里面有foldr嘛。那让我们来看看同样有foldr(reverse + aggregate = foldr)的C#会怎么写:
T SuperApply<T>(Func<T, T>[] fs, T x)
{
return (fs
.Reverse()
.Aggregate(x=>x, (a, b)=>y=>b(a(y)))
)(x);
}
C#基本上已经达到跟Haskell一样的描述过程了,而且也可以写出下面的代码了,就是无论声明和使用的语法的噪音稍微有点大……
SuperApply(new Func<T, T>[]{
x=>x*x,
x=>x*2,
x=>x+1
}, 1);
为什么要在讨论语法的一致性的时候说这些问题呢,在这里我想向大家展示Haskell的另一种“定义和使用相一致”的做法。Haskell整个语言都要用pattern matching去理解,所以上面的这段代码
superApply fs x = (foldr id (.) fs) x
说的是,凡是你出现类似superApply a b的这种“pattern”,你都可以把它当成(foldr id (.) a) b来看。譬如说
superApply [(\x->x*x), (*2), (+1)] 1
其实就是
(foldr id (.) [(\x->x*x), (*2), (+1)]) 1
只要superApply指的是这个函数,那无论在什么上下文里面,你都可以放心的做这种替换而程序的意思绝对不会有变化——这就是haskell的带有一致性的原则。那让我们来看看Haskell是如何执行他这个一致性的。在这里我们需要知道一个东西,就是如果我们有一个操作符+,那我们要把+当成函数来看,我们就要写(+)。如果我们有一个函数f,如果我们要把它当成操作符来看,那就要写成`f`(这是按键!左边的那个符号)。因此Haskell其实允许我们做下面的声明:
(Point x y) + (Point z w) = Point (x+z) (y+w)
(+) (Point x y) (Point z w) = Point (x+z) (y+w)
(Point x y) `Add` (Point z w) = Point (x+z) (y+w)
Add (Point x y) (Point z w) = Point (x+z) (y+w)
斐波那契数列的简单形式甚至还可以这么写:
f 1 = 1
f 2 = 1
f (n+2) = f(n+1) + f(n)
甚至连递归都可以写成:
GetListLength [] = 0
GetListLength (x:xs) = 1 + GetListLength xs
Haskell到处都贯彻了“函数和操作符的替换关系”和“pattern matching”两个原则来做“定义和实现相一致”的基础,从而实现了一个比C语言那个做了一半的混乱的原则要好得多的原则。
有些人可能会说,Haskell写递归这么容易,那会不会因为鼓励人们写递归,而整个程序充满了递归,很容易stack overflow或者降低运行效率呢?在这里你可以往上翻,在这篇文章的前面有一句话“好的语言,除了库写起来又容易又好用以外,还有两个重要的特点:容易学,容易分析。”,这在Haskell里面体现得淋漓尽致。
我们知道循环就是尾递归,所以如果我们把代码写成尾递归,那Haskell的编译器就会识别出来,从而在生成x86代码的时候把它处理成循环。一个尾递归递归函数的退出点,要么是一个不包含自身函数调用的表达式,要么就是用自身函数来和其它参数来调用。听起来比较拗口,不过说白了其实就是:
GetListLength_ [] c = c
GetListLength_ (x:xs) c = GetListLength_ xs (c+1)
GetListLength xs = GetListLength_ xs 0
当你写出这样的代码的时候,Haskell把你的代码编译了之后,就会真的输出一个循环,从而上面的担心都一扫而空。
实际上,有很多性能测试都表明,在大多数平台上,Haskell的速度也不会被C/C++慢超过一倍的同时,要远比go的性能高出许多。在Windows上,函数式语言最快的是F#。Linux上则是Scala。Haskell一直都是第二名,但是只比第一名慢一点点。
为了不让文章太长,好分成若干次发布,每次间隔都较短,所以今天的坑我只想多讲一个——C++的指针的坑。剩下的坑留到下一篇文章里面。下面要讲的这个坑,如果不是在粉丝群里面被问了,我还不知道有人会这么做:
class Base
{
...
};
class Derived : public Base
{
...
};
Base* bs = new Derived[10];
delete[] bs;
我想说,这完全是C++兼容C语言,然后让C语言给坑了。其实这个问题在C语言里面是不会出现的,因为C语言的指针其实说白了只有一种:char*。很多C语言的函数都接受char*,void*还是后来才有的。C语言操作指针用的malloc和free,其实也是把他当char*在看。所以当你malloc了一个东西,然后cast成你需要的类型,最后free掉,这一步cast存在不存在对于free能否正确执行来说是没有区别的。
但是事情到了C++就不一样了。C++有继承,有了继承就有指针的隐式类型转换。于是看上面的代码,我们new[]了一个指针是Derived*类型的,然后隐式转换到了Base*。最后我们拿他delete[],因为delete[]需要调用析构函数,但是Base*类型的指针式不能正确计算出Derived数组的10个析构函数需要的this指针的位置的,所以在这个时候,代码就完蛋了(如果没完蛋,那只是巧合)。
为了兼容C语言,“new[]的指针需要delete[]”和“子类指针可以转父类指针”的两条规则成功的冲突到了一起。实际上,如果需要解决这种问题,那类型应该怎么改呢?其实我们可以跟C#一样引入Derived[]的这种指针类型。这还是new[]出来的东西,C++里面也可以要求delete[],但是区别是他再也不能转成Base[]了。只可惜,T[]这种类型被C语言占用了,在函数参数类型里面当T*用。C语言浪费语法罪该万死呀……
Ⅱ 编程语言是如何创建的
因为有解释器啊,编译型的语言一般都被转化成底层的机械码了。要想自己开发语言完全是没问题的。至于看什么《编译原理基础》感觉价值不大,能真正看懂的人太少了。还不如直接找个开源的程序语言研究一下。至少改个语法应该没问题。
Ⅲ 可以自己创造一种语言吗
语言都是人创造出来的 所以答案是肯定的但是 前提是在以拥有的语言中才能创造 当然 也可以当版作是自己的暗号不权过呢 如果只有自己才能看懂的那就不叫语言了 语言的意思简单来说就是人与人通过说看写来传达自己的意思 没有人看的懂也就不能成为语言 只能说是暗号中国的词汇量不是一般的大 若是毅力顽强的你执意要去做 希望你能成功
Ⅳ 如何自创一门编程语言
我也有这种想法,不过我查过很多资料,要想自己创造一门编程语言(我说的内是中文编程语言),需要从头开始容,比如,从机器代码开始,创造出中文的汇编语言,然后从中文汇编语言创造出高级中文编程语言。编程语言是为了让计算机知道我们到底想让它做些什么。所以从头开始是最简单的事情,但也是最难的事情,单单是其中低级转到高级语言中的转换解释就需要很多知识。我说的不是那种把英文单词改成中文单词那种。而是改变结构,语法分析等等。现在我还在研究,但是很多东西真的不懂。因为不是母语,所以看不懂英文教材。也记不住基本的英文指令代码。更加不理解到底是什么意思。哎。加油。
Ⅳ 如何自己创造一门语言
创造不是很难,难的是被别人采用。语言是交际的工具,一个人创造的要是没人采用,有什么用?
创造一套密电码还差不多。
Ⅵ 如何自创一门语言
这个是不可能的,创造语言需要经历长时间的发展与很多人的加入与发展。
Ⅶ 一门编程语言是怎么创造出来的呢
所谓计算机语言只是一个抽象的规范,而编译器是这个规范的实现,它是在这内个规范的严格定义下容被实现的.
说的通俗一点,语言就像一份制造汽车的图纸,他规定了
汽车应该有什么,拥有什么功能,等等各个方面,而编译器就是一量根据这张图纸制造出来的汽车,它实现了图纸的定义,这样你才能真正的使用这量汽车.
如果你有OOP的方面的知识,那可以把语言和编译器比喻成类和对象的关系.
他们谁也离不开谁,只有两部分共同合作,你才能使用这个语言.
Ⅷ 如何自创一门编程语言
从你的描述来看,自创一门编程语言是非常困难的。现在的编程语言都是几十年前专研属究人员做出来的,他们为编程语言做出了很大的贡献。
如果自创一门编程语言的话,几乎是不可能的,就算一个国家,集一国之力也很难做出来。
Ⅸ 人类语言是怎样产生的
需要特别提出的是语言的产生以及它对脑髓发展的影响。“语言是从劳动中并和劳动一起产内生出来的”。原容始人类在狩猎、采集植物、制造石器等活动中,需要共同协作,“已经到了彼此间有些什么非说不可的地步了”,于是便产生了一定音节和一定内容相结合的语言。
最初的语言起初的语言自然是十分简单的,后来随着劳动的不断发展才逐步完善和丰富起来。在语言发展的过程中,古猿的发音器官——喉管也逐渐改造成为人的喉管。
语言的作用语言是表达思想的工具,由于语言的产生,使人们彼此间可以交流经验,因此获得的间接经验就更多。在文字产生以前,语言对人类知识的传播和积累,从而对人类认识世界和改造世界,有着重大作用。“有声语言在人类历史上是帮助人们脱出动物界、结成社会、发展自己的思维、组织社会生产、同自然力量作胜利的斗争并取得我们今天的进步的力量之一。”
Ⅹ 怎么研发一款编程语言
编程语言,作为人与计算机沟通的桥梁,有着重要和深远的意义。有过计算机编程经验的人,多少学习或掌握过一到多种编程语言。计算机专业领域的编程语言成百上千种,主流的编程语言也有数十种之多。每种编程语言面向的领域和特性都不尽相同,不过归根结底是为了解决人与计算机之间沟通的效率问题,提高计算机的生产力。想必有不少人对那些主流编程语言的创造者十分倾佩,也相信有不少人会好奇一门编程语言是如何诞生的。那么如何创造一门编程语言呢?
总的来看,创造一门编程语言需要有以下几个过程:
(1)设计语言的特性。
(2)定义语言的单词、语法和语义。
(3)实现编译器或者解释器将程序翻译为计算机底层表示。
(4)生成计算机程序的二进制存储格式。
(5)完善语言的运行时环境和标准库。
一、语言特性设计
所谓语言特性,就是编程语言为开发者提供了什么样的原子性功能特征。比如是否支持数学表达式计算、字符串处理,是否支持变量、函数和递归,是否支持分支、循环复合语句等。语言的变量类型是强类型、弱类型,还是动态类型,程序是过程式、函数式,还是面向对象的。是否支持模板、泛型和反射机制,是否支持多线程和并发特性,是否支持错误和异常处理机制等等。
语言特性设计是一门编程语言最关键的环节,直接决定了语言的基本特征和雏形。当然,这也是最难的一个环节,因为语言设计是面向具体问题领域的,是语言设计者从大量的编程实践中的获得的总结和升华。比如C语言设计者希望面向计算机底层,拥有对操作系统和硬件的直接操纵能力。而Python的设计者则希望尽可能地减少操作计算机资源的繁琐过程,以获得语言的简洁性、高度的灵活性和扩展性。SQL的设计者面向具体的数据查询和分析领域,希望帮助开发者获得快速检索和操纵数据的能力。而Go语言的设计者则希望在保留C语言优秀功能的基础上,扩展编程语言对高并发环境的支持,并拥有垃圾回收和快速编译的能力。
凡此种种,编程语言特性的设计都是面向具体的问题领域的,是语言设计者构建于开发者和计算机之间的中间层,是对开发过程中重复功能逻辑的原子性“封装”,最终的目的是为了提升具体问题领域内的软件开发效率。
二、单词、语法和语义
和人类使用的自然语言类似,编程语言也有自身的单词、语法和语义,专业上称为词法记号、语言文法和语义。
常见的词法记号可以分为数字、字符、字符串、标识符、关键字,以及用于连接表达式的运算符、分割语句或者程序段落的界符等符号。这些是编程语言程序的基本单位,通过它们的有序组合,构建出了一门编程语言形形色色的代码片段。
编程语言的文法是用来描述语言的语法规则的,具体来说是规定词法记号之间的排列组合的顺序与规则。它描述了编程语言程序的基本模式,不符合该模式的词法记号的排列被挡在了合法语言程序的大门之外。同时,它也是各种编程语言对于开发者最明显的差异化特征。一个有经验的开发者可以很容易地通过扫视一段代码,就能分辨出这是哪种编程语言编写的计算机程序。
编程语言的语义描述了一段符合语言语法的程序,对于计算机而言的真正含义,是开发者最终要传达给计算机的意愿和指令。语言的语义必须是准确的、无二义性的,编译器也正是通过语义的指导,将计算机程序翻译为计算机可识别的表达形式。
三、程序的翻译
计算机程序是用来供人阅读和修改的,计算机硬件并不能理解程序内的思想和含义。因此,必须有一个翻译转换的过程,将人所表达的意愿准确无误地传递给计算机,让计算机明确并执行人下发的指令。实现这种翻译工作的工具就是编译器或解释器。
对于编译器来说,它的输入是人类书写的计算机语言程序,输出则是计算机可识别的底层表示。首先,它需要识别出程序中的单词,即词法分析。然后,根据单词的组合模式识别出程序的语法结构,即语法分析。最后,根据不同的语法结构对应的语义,将程序按照每个语法模块的形式转换为计算机可识别的指令序列,即语义分析和目标代码生成。
众所周知编译器的实现具有一定的复杂度,其根本原因来自于语言语法的结构灵活性和计算机底层表达形式的多样性,这也是创造一门编程语言最核心的环节。
四、二进制存储
编译器将语言程序翻译转换后,需要将转换后的结果存储起来,以便计算机在需要的时候将其加载、执行。这里不可避免的涉及到两个问题:
(1)转换后的结果是什么样的形式?
(2)转换后的结果保存在哪里?
第一个问题描述的是计算机程序被转换为怎样的形式,才是计算机可以识别的。由于计算机中实际运行程序的硬件模块是CPU,因此计算机程序只有被转换为CPU的二进制指令格式才能被正确识别、执行。比如常见的Intel体系的CISC指令格式、ARM体系的RISC执行格式等。
第二个问题描述的是计算机程序转化为二进制指令格式后,以什么样的方式保存在计算机的磁盘上。由于绝大多数的计算机程序是需要通过运行在计算机硬件之上的操作系统加载运行的,因此计算机程序的二进制表达形式必须以对应操作系统可识别的文件格式存储。比如常见的Windows操作系统的PE文件格式、Linux操作系统的ELF文件格式等。
五、运行时环境和标准库
理论上讲,一门编程语言如果能提供出完备的操纵操作系统和硬件的原子性功能就已经成功了。但是不提供强大的运行时环境支持和标准库,是很难让一门编程语言真正的好用和流行的。没有人希望简单地打印一行字符串,还需要使用编程语言提供的基本特性实现调用操作系统提供的打印接口的逻辑。Java语言之所以久兴不衰,正是因为它不仅提供了完善的运行时环境和开发库支持,甚至提供了更强大的开发框架和工具支持。
因此可见,除了完备的语言特性,为开发者提供更方便好用的库和框架支持,消除软件构建过程中复杂和重复的逻辑,才是一门优秀编程语言的长盛之道。
六、自己动手,立即开始!
《自己动手构造编译系统——编译、汇编与链接》一书详细阐述了一门编程语言从无到有的过程,从语言的功能特性设计,到词法、文法、语义分析;从编译器、汇编器的设计实现,到目标文件的链接生成可执行文件;甚至编译优化器的实现、二进制指令、可执行文件格式以及语言运行时和标准库的概念,都在书中做了认真细致地剖析。相信对本书的阅读,将是一次不错的获得知识的体验!