不同编程语言有不同的语法,语法主要影响可读性与编写效率。大部分语言的语法都比较相近,但一些语法设计上的细微差异会对编程过程产生不同的体验(特指程序员的心情,不是指思考方式、算法实现等理论知识)。

本文会提及如下语言C++、rust、go、python、ruby、haskell、lisp,但其中rust、go仅仅学过语法,没有实践过,所以观点非常片面!所以观点非常片面!所以观点非常片面

前言 Link to heading

不同语言有不同的使用领域,这主要取决于他们的底层实现、语言特性以及领域上的支持(指开发工具、库等)。比如elixir特别适用于大量并发和软实时的场景,python用于系统脚本、数据科学等领域,perl主攻文本处理等系统工具,ruby的RoR等。

本文想要讨论的是语法社区风格一些特性的实现方式,而不是所有的语言特性。我将要讨论的内容主要是程序的表达/表述方式在编写过程中的快感、便利性等,这不在上述内容中。

表达/语法 Link to heading

对编程语言的语法,除了解析外,另外一点就是阅读与书写。也就是其表达能力。我们在这里说的不是单纯表面的语法规则。语法规则大家都是比较相近的,讨论这些没有意义。重点是语法之下带来阅读与书写的体验。同时语法带来的一定程度思维的导向。

不同语言都有相类似的控制结构、类型结构、函数定义等方式,他们在本质上是相类似的。不同的语言使用不同的实现方式带来了不同的特性,比如haskell的惰性计算带来了函数便利的composition,lisp的list式的语法拥有非常强大的macro等。ruby和perl使用大量了语法糖简化了相同目的代码编写,并且提供了不同的编码风格。python用较为简单(单一)的语法来减少程序员(编写过程时)思考的负担。这些设计提供了不同的编码风格与哲学,如There’s more than one way to do itThere should be one—and preferably only one—obvious way to do it,metaprogram等。

非常显然,perl、ruby这种语法糖的设计为编写带了非常多的乐趣。因为我们可以自由的选择代码的编写风格。而python就相对死板,在大部分场景,我们仅有唯一的方案来实现需求。动态语言带来了变量、对象便利地创建与调用,但又引入了调试时较难追踪的问题。同时,java/c++的静态类型有一定安全保证,可使得在声名函数、临时定义变量等方面稍显冗余(为了处理这一问题,引入了一些语法糖,比如c++11的trailing、auto等)。rust/go意在解决安全/并发问题,使用简洁的语法带来了干净的代码,但简化就意味着需要编写更多的代码(但得到了性能的提升,这种时候这一问题也不是问题,毕竟每条语句对应更少的机器码)。lisp主要是命名风格有点糟糕,因为缺少描述语义的类型(特指类、结构,虽然cl支持对象,但还是有点区别),为了避免冲突或者描述函数功能时使用命名显得非常非常长,同时,冗余的语法带来的感观的痛苦(虽然理解起来不难)。

R语言在data.frame使用了attche等手法来简化了列的选择,并且可以使用多种方式来赋值。这种针对R主要用于数据分析而设计的语法就非常的合适与灵活。

冗余 Link to heading

适当的冗余可以提高代码的阅读效率,比如ruby、perl等使用一些符号或者关键字来提示信息,同时这种符号也可以用作上下文信息的选择。但冗余不应该过度(指明明一个静态语言,却还在变量定义时需要指明类型,明明已经调用类的初始化函数)。

符号表达 Link to heading

如python,使用if-else做为三元操作,同时还有andor等作为逻辑操作符,这更多的习惯问题,但是这种纯文本真的必要么?适当使用符号表达可以提高代码的整洁度。符号会在一定程度上对阅读代码的人产生负担。因为必需去符号的定义有一定了解。但是符号在阅读时可以产生分隔的效果,非常明显的分离子表达式。当然关键字高亮也可以做到同样的事,但直观上不如符号清晰。更加激进,我们可以各自使用符号,比如haskell,符号还充当了语法解析时的分隔符。并且使用符号可以将一部分频繁使用的函数表达弱化,从而加快理解,比如>>=<$><*>。当然,这无疑增加了程序员的负担,可是一定程度的负担可以提高对整理程序的表达,我认为这是值得的(个人比较喜欢这一口,但我也明白,这是比较奇怪的喜好,相当一部分人应该讨厌这种)。

强制缩进 Link to heading

缩进是必然的,因为为了使语言可读,但python这种强制缩进就显得很微妙,因为全文缩进无处不在。haskell的缩进针对不同上下文可以自由缩进大小,因为在一个块内其缩进必然是相同的,但不同块间的缩进可能产生差异。python这一点与haskell相同,可是haskell是一个函数式语言,他编写长段代码的时间要小于命令式语言,更多的部分是可能函数组合。单子用得非常多时,缩进方面却也没有python带来的那种异样感。原因可能出在haskell缩进大多时候只有一层至二层。而python如果需要减少层级就得定义额外的函数调用,在haskell里,通过where额外函数定义会非常方便,但python中虽然有局部函数的作用域,但这反而会加长函数体的长度,lisp也是同理,辅助函数不得不进行分离。而且需要注意到,haskell的where语块,一般是放在函数末尾的,就是说辅助函数定义不会影响到阅读当前函数。而对lisp与python,在函数当前的作用域内定义就难以避免去影响阅读代码,在外部定义,又会污染环境(在上层环境命名辅助函数有时有非常痛苦的,并且辅助函数通常只有本函数会用到,语义明确的场合姑且不论,当层级过深不得不分离层级却又不存在明显的语义时,这就比较难受了。当然,后者的情况大概率是抽象不够或者耦合过高导致的)。

语法糖 Link to heading

同时,语法糖不是说有就好,语法糖应该要适当。perl、ruby、haskell的语法糖就显得非常合适,可以选择自己喜欢的,又确实简化了编写。

比如haskell使用if-then-else来简化case-of-True-False,更进一步,可以使用pattern match来避免if-then-else,还提高了语义。ghc提供了相当一部分语法扩展以供选择。

语法糖的使用可能会产生编程风格的差异,但是这对编写过程来说是轻松的。通过部分记忆也能增加对程序的理解。我个人比较喜欢这种风格,而不是python那种限制语法糖的做法,每种类型的表达式只有一个。

糖的关键不在于有多少以及有多么复杂,而是在这个语言下,可以把重复无趣的编码用简洁、明了的方式进行展现。这种表达能力可以有表达式本身的,也可以有整体结构方面的。

语法糖非常重要的一点是合适,比如haskell(pros/cons),选择合适的糖与扩展,提高了整体的编程体验。其根本目的是将重复的编写工作用简单的语法代替。perl和ruby的语法糖也是如此,多且实用。可以将无趣的重复工作简化的语法,为什么不爱呢?python无疑就需要书写这种代码。

当然,python也有糖,比如数组的遍利返回、装饰器等,问题在于这点糖根本不够。在编写一些代码的时候,就会显得死板、无趣。不如说python的语法糖感觉有种奇怪的设计,糖的重点不在于表达的结构,而在更关注于定义方面。

社区习惯 Link to heading

语法糖之外就是社区习惯。比如nodejs和go这类静态链接(nodejs类似于静态链接,他每个包的依赖是独立的,而不是共有的),优势的性能之外就是非常冗余的依赖库(因为性能好的情况,这种冗余也无所谓就是了)。python强调的设计哲学,带来了较为规范的编码风格,但同时也显得很无趣。

实现方式 Link to heading

最后想写在这里的是特性的实现方式。这里点名批评python的magic method。我能理解使用__<name>__来分区一般方法,但这种代码非常影响视感,有种割裂的感觉。而c++/ruby的重载就显得真观和舒适。另外一点主要想写写关于metaprogram,python的metaprogram是基于原语法上,使用对象的方式来meta,听上去很美好。写起来,也不算太差。但相较之下ruby写入语法本向的一些meta手段,就很让人清爽。同样,lisp因为其本身语法所以metaprogram非常方便。C家庭的宏,呃,这个有点特殊,本质上是编译时的字符替换,不太能算在语法上面吧(太过于简单了,简单到不知道要怎么评价,它带来了不少问题,但也确实简化了不少操作,还非常的轻)?不过C的宏确实能实现不少trick,但在理解上产生了一定困难,因为宏的用法有点多(比如以前MFC封闭的宏,非常强大、整洁,但到处都是)。haskell/ghc的语言扩展实现了generic和template,这二者有点复杂,因为需要对ast有一定理解才可以使用,但正因为可以直接操作ast了,所以可能像lisp那样非常方便的编写各种宏,可毕竟lisp宏的便利是建立在其语言特性(指list)上的,自然haskell这种meta写起来有一定复杂程度,可用起来会较为轻松。至于c++等语言的模板,它在一定程度上处理了面向对象语言处理基本类型不同、类结构与语义相同情形下的复用问题,算是一种静态语言的折中,且确实提高了复用,就不多做评价了。只能说是好用的程度,没差到让人难受,但也没强到让人喜欢。

一些可能无关的碎碎念及对比 Link to heading

生硬感 Link to heading

上述中没有提及的一点是编写代码的“生硬”感。这一问题,有语法糖的问题,但更多的是语言本身设计范式产生的。在c++里,很难先对一个容器filter后,再map/foreach(在最新的c++20标准里已经有了)。我们当然可以在循环时用条件来过滤,过滤完直接做操作,而不是将这两个操作分离,这也是大多数命令式语言的做法。这种生硬感受人群而定。

此外,虽然大家语言都能写同样的功能,但没有一些糖和特性,有些功能写出来就感觉非常的生硬,他直观,但很冗余。这种冗余多数时间还是不必要的。

可能写习惯的函数式的人,会更喜欢后者,而一些人喜欢前者。两种方法都非常直观(在不同意思上的),并无优劣。

小结 Link to heading

本文主要讨论的是编写代码的观感。并不是语言的设计思想、核心特性。这种观感主要影响的是我们自己选择自用语言,以及拥有同样或相似设计的语言时,如何选择。一句话就是我希望写的开心,不要有那种无趣的重复。在这之前,我们要考虑的还有语言的范式、基础特性。可能这些基础的特性,就已经提供了足够了表达能力,语法糖变得相对次要。

这一篇文章其实没什么实质内容,完全是写给自己的一篇“说服”文。在语言的功能上,大家其实都差不多了。只有核心特性和社区支持有些差异。但写得开心、自己满意,真的很重要。